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.
This commit is contained in:
0xrsydn 2026-09-22 15:26:59 +07:00
commit 9696282110
10 changed files with 476 additions and 123 deletions

View file

@ -20,6 +20,7 @@ from __future__ import annotations
import contextlib
import io
import hashlib
import itertools
import json
import os
@ -125,10 +126,11 @@ class FakeSts2:
return types.SimpleNamespace(ok=ok, message="rejected" if not ok else "")
def invoke(fake, *flags, client=None, clock=None, decide=None):
def invoke(fake, *flags, client=None, clock=None, decide=None, directory=None):
"""Run the real loop with isolated files, no delays, and no live services."""
callbacks = []
with tempfile.TemporaryDirectory() as directory, contextlib.ExitStack() as stack:
with contextlib.ExitStack() as stack:
if directory is None:
directory = stack.enter_context(tempfile.TemporaryDirectory())
capdir = pathlib.Path(directory) / "capture"
stack.enter_context(patch.object(run, "sts2", fake))
stack.enter_context(patch.object(run, "JevClient", return_value=client or StubClient()))
@ -138,7 +140,6 @@ def invoke(fake, *flags, client=None, clock=None, decide=None):
legacy = pathlib.Path(directory) / "deck.json"
legacy_text = '{"counts": {"STALE CARD": 999}}'
legacy.write_text(legacy_text)
stack.enter_context(patch.object(run.atexit, "register", side_effect=callbacks.append))
stack.enter_context(patch.object(run.time, "sleep"))
stack.enter_context(patch.object(sys, "argv", ["run.py", "--steps", "10", "--pause", "0",
"--capture-dir", str(capdir), *flags]))
@ -148,13 +149,15 @@ def invoke(fake, *flags, client=None, clock=None, decide=None):
stack.enter_context(patch.object(brain, "decide", side_effect=decide))
output = stack.enter_context(contextlib.redirect_stdout(io.StringIO()))
rc = run.main()
for callback in callbacks:
callback()
def rows(name):
path = capdir / name
return [json.loads(line) for line in path.read_text().splitlines()] if path.exists() else []
return types.SimpleNamespace(rc=rc, rows=rows("decisions.jsonl"),
sessions=rows("sessions.jsonl"), saved=int(not legacy.exists() or legacy.read_text() != legacy_text),
sessions = rows("sessions.jsonl")
events = [event for session in sessions for event in rows(session["recording_path"])]
blobs = {event["observation_id"]: (capdir / event["path"]).read_bytes()
for event in events if event["event"] == "observation"}
return types.SimpleNamespace(rc=rc, rows=rows("decisions.jsonl"), events=events, blobs=blobs,
sessions=sessions, saved=int(not legacy.exists() or legacy.read_text() != legacy_text),
output=output.getvalue())
@ -173,9 +176,9 @@ print("=== 2. a decision that asked nothing logs jev: null ===")
stub = run.RecordingClient(StubClient())
code_decision = brain.Decision("end_turn", {}, "no playable cards", "code")
row = run.decision_record(1, "monster", {"act": 1, "floor": 1}, code_decision,
None, stub)
None, stub, session_id="fixture-session")
check("jev is null", row["jev"], None)
check("the session id travels with the row", row["session"], run.SESSION_ID)
check("the session id travels with the row", row["session"], "fixture-session")
check("the action is still recorded", row["action"], "end_turn")
print()
@ -212,7 +215,7 @@ check("...with its value and gate outcome, not just a yes/no",
rows[0]["jev"]["answers"]["good_card0"]["gated"]), (0.9, True))
check("row 2 asked nothing", rows[1]["jev"], None)
check("...and is a code decision", rows[1]["source"], "code")
check("every row carries the session", {r["session"] for r in rows}, {run.SESSION_ID})
check("every row carries the session", {r["session"] for r in rows}, {result.sessions[0]["session"]})
print()
print("=== 4. the session row maps to an outcome (join half) ===")
@ -457,6 +460,60 @@ for label, states, identities in [
evidence = [state["deck_composition"] for state, _ in client.requests if "deck_composition" in state]
check(label + " receives unknown deck context", evidence[-1], "unknown")
print()
print("=== 10. recordings link observations, proposals, attempts, and results ===")
states = [game_over, reward_here, reward_here, {"state_type": "overlay"}, game_over]
client = StubClient(.1)
fake = FakeSts2(states, action_ok=[True, False, True, True])
result = invoke(fake, "--steps", "4", "--stop-on-run-end", client=client)
observations = {e["observation_id"]: e for e in result.events if e["event"] == "observation"}
proposals = {e["proposal_id"]: e for e in result.events if e["event"] == "proposal"}
attempts = {e["attempt_id"]: e for e in result.events if e["event"] == "action_attempt"}
results = [e for e in result.events if e["event"] == "action_result"]
check("full preflight and loop observations survive unchanged",
[json.loads(result.blobs[key]) for key in observations], states)
check("every stored observation matches its SHA-256 reference",
all(hashlib.sha256(result.blobs[key]).hexdigest() == event["sha256"]
for key, event in observations.items()), True)
check("every proposal and attempt references its exact input observation",
all(p["observation_id"] in observations for p in proposals.values()) and
all(a["proposal_id"] in proposals and a["observation_id"] == proposals[a["proposal_id"]]["observation_id"]
for a in attempts.values()), True)
check("results join one-to-one with actual attempts, including both system dismissals",
([e["attempt_id"] for e in results], [e["outcome"] for e in results]),
(list(attempts), ["accepted", "rejected", "accepted", "accepted"]))
check("next observations link attempts without asserting their effects",
[e["after_attempt_id"] for e in observations.values()], [None, *list(attempts)[:3], None])
check("terminal dismissal honestly has no subsequent observation",
result.sessions[0]["awaiting_observation_after_attempt"], list(attempts)[-1])
check("wait decisions are recorded without action attempts",
([p["action"] for p in proposals.values()].count("__wait__"), len(attempts)), (1, 4))
check("successful model records include the exact request state",
[p["jev"]["state"] for p in proposals.values() if p.get("jev")], [s for s, _ in client.requests])
check("session finalizes exactly once before main returns",
(len(result.sessions), sum(e["event"] == "session_end" for e in result.events)), (1, 1))
with tempfile.TemporaryDirectory() as directory:
first = invoke(FakeSts2([menu]), "--steps", "2", "--no-jev", "--dry-run", directory=directory)
second = invoke(FakeSts2([menu]), "--steps", "2", "--no-jev", directory=directory)
check("repeated main calls use distinct session IDs in one capture directory",
len({s["session"] for s in second.sessions}), 2)
check("later sessions never overwrite earlier observations",
{key: second.blobs[key] for key in first.blobs}, first.blobs)
check("dry-run records previews, never attempts or execution-based suppression",
[e["status"] for e in first.events if e["event"] == "proposal_status"], ["dry_run", "dry_run"])
check("duplicate suppression is visible as a proposal disposition",
[e["status"] for e in second.events if e["event"] == "proposal_status"][-2:], ["execute", "suppressed"])
for error, expected_code, reason in [(FakeSts2.Sts2Error("connection closed"), 1, "action_error"),
(KeyboardInterrupt(), 130, "interrupted")]:
fake = FakeSts2([menu], action_error=error)
result = invoke(fake, "--no-jev")
check(f"{reason}: one attempt, unknown outcome, synchronous finalization",
(result.rc, len(fake.actions), [e["outcome"] for e in result.events if e["event"] == "action_result"],
result.sessions[0]["stop_reason"], sum(e["event"] == "session_end" for e in result.events)),
(expected_code, 1, ["unknown"], reason, 1))
print()
print(f"=== {PASS} passed, {FAIL} failed ===")
sys.exit(1 if FAIL else 0)