Files
bench/tests/test_session_identity.py
istosandClaude Opus 5 f428ee0468 sessions: persist who a session was, so a replayed run is not "You"
"You" was the else-branch of session_label: anything the board could not
attribute to an agent it attributed to the person. Every session read back
from disk was one of those, because the agent id lived only in the session
registry and never reached the persisted events — so past agent runs came
back wearing the human's label, carrying their own closing reports
underneath it.

Identity is now a small whole file beside each event log
(state/sessions/<id>.who.json): agent id, the agent's name, the model it
rode, and the task. A file rather than a key on the events, because the
logs are append-only JSONL whose first line every reader takes for an
event — and because the name and the model are nowhere in the stream, so
this is the only thing a restart can read them back from. It is rewritten
only when what the board knows changes, which also covers an agent id that
arrives on a later event.

load_disk_sessions() reads it back, and the label now has three registers
instead of two: the agent's name (persisted, so a restart no longer costs
it), "You" only for a session positively recorded as carrying no agent,
and a neutral "Session · <id>" for a log written before any of this was
recorded. Old logs are not retro-attributed in either direction.

agentFor() in board.html falls back to the persisted identity when this
board no longer holds the live record, so a replayed agent session wears
its model chip from what was written rather than from what happens to be
in memory. What depends on liveness (Hold, the worktree branch) finds
nothing there and stays silent, as before.

tests/test_session_identity.py drives the real ingest → persist → reload
path and the page's own chip functions in node.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 16:35:19 +02:00

317 lines
14 KiB
Python

"""A session remembers who it was (task 45).
`You` used to be the label the board reached for when it could not
attribute a session — and every session read back from disk was one of
those, because the agent id lived only in memory. Past agent runs came
back wearing the person's name.
So identity is persisted beside the event log, in a small whole file of
its own: the agent id, the agent's name and the model it rode (neither of
which is anywhere in the event stream), and the task. The label then has
three registers instead of two — the name, `You` for a session positively
known to have carried no agent, and a neutral `Session` for a log written
before any of this was recorded.
These tests drive the real ingest → persist → reload path with the
sessions directory pointed at a temporary one; the browser half (a
replayed run wearing its model chip) is the page's own functions run in
node, as in test_model_chip.
python3 -m unittest discover -s tests -v
"""
from __future__ import annotations
import json
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO / "manager" / "core"))
import config # noqa: E402
import events # noqa: E402
import state # noqa: E402
BOARD = REPO / "manager" / "core" / "board.html"
TASK = "45-a-past-agent-session-is-not-you.md"
class Sessions(unittest.TestCase):
"""One temporary sessions directory; each test writes its own history."""
def setUp(self):
self.tmp = Path(tempfile.mkdtemp(prefix="bench-identity-")).resolve()
self.addCleanup(shutil.rmtree, self.tmp, True)
self.addCleanup(setattr, config, "SESSIONS_DIR", config.SESSIONS_DIR)
config.SESSIONS_DIR = self.tmp / "sessions"
for registry in (state.SESSIONS, state.EVENTS, state.AGENTS,
events._WRITTEN):
registry.clear()
self.addCleanup(registry.clear)
state.BOARD_EVENTS.clear()
self.addCleanup(state.BOARD_EVENTS.clear)
self.addCleanup(setattr, state, "broadcast", state.broadcast)
state.broadcast = lambda payload: None
# ── the world the board sees ──────────────────────────────────────────
def launch(self, agent_id: str, name: str, model: str | None,
task: str = TASK) -> dict:
"""A launch record, as agents.py registers one."""
record = {"id": agent_id, "task": task, "name": name, "model": model,
"session": None, "status": "running", "mode": "work"}
state.AGENTS[agent_id] = record
return record
def ingest(self, sid: str, summary: str, agent: str | None = None,
task: str | None = None, kind: str = "edit") -> None:
event = {"v": 1, "session": sid, "kind": kind, "summary": summary}
if agent:
event["agent"] = agent
if task:
event["task"] = task
events.ingest_event(event)
def restart(self) -> None:
"""What a board restart costs: every registry goes, the disk stays."""
for registry in (state.SESSIONS, state.EVENTS, state.AGENTS,
events._WRITTEN):
registry.clear()
events.load_disk_sessions()
def label(self, sid: str) -> str:
return state.SESSIONS[sid]["label"]
# ── the bug ───────────────────────────────────────────────────────────
def test_an_agent_session_replayed_after_a_restart_is_still_the_agent(self):
"""Acceptance 1: the row carries the agent's name and task, not You."""
self.launch("45-a-past-agent-120000", "Nell", "claude-opus-4-8")
self.ingest("sess-nell", "editing events.py", agent="45-a-past-agent-120000")
self.ingest("sess-nell", "Nell's report on " + TASK, kind="report",
agent="45-a-past-agent-120000")
self.assertEqual(self.label("sess-nell"), "Nell · #45")
self.restart()
meta = state.SESSIONS["sess-nell"]
self.assertEqual(meta["label"], "Nell · #45")
self.assertEqual(meta["task"], TASK)
self.assertEqual(meta["agentId"], "45-a-past-agent-120000")
self.assertEqual(meta["agentModel"], "claude-opus-4-8")
def test_a_human_session_replayed_from_disk_still_reads_you(self):
"""Acceptance 2: this fix does not relabel the person's own work."""
self.ingest("sess-mine-0123456789", "reading AGENTS.md")
self.assertEqual(self.label("sess-mine-0123456789"), "You · sess-min")
self.restart()
self.assertEqual(self.label("sess-mine-0123456789"), "You · sess-min")
def test_a_log_from_before_identities_were_recorded_claims_nothing(self):
"""Acceptance 3: no identity on disk means unknown, not you. Old
logs are not retro-attributed in either direction."""
config.SESSIONS_DIR.mkdir(parents=True)
(config.SESSIONS_DIR / "sess-oldrun.jsonl").write_text(
json.dumps({"ts": 1, "session": "sess-oldrun", "kind": "edit",
"summary": "editing board.html"}) + "\n"
+ json.dumps({"ts": 2, "session": "sess-oldrun", "kind": "report",
"summary": f"Piper's report on {TASK}"}) + "\n",
encoding="utf-8")
events.load_disk_sessions()
meta = state.SESSIONS["sess-oldrun"]
self.assertEqual(meta["label"], "Session · sess-old")
self.assertNotIn("You", meta["label"])
self.assertIsNone(meta["agentId"])
def test_restarting_the_board_changes_no_label(self):
"""Acceptance 4, across all three registers at once."""
self.launch("review-45-a-past-agent-130000", "Piper", None)
self.ingest("sess-piper", "reading the card",
agent="review-45-a-past-agent-130000")
self.ingest("sess-mine", "running the tests")
config.SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
(config.SESSIONS_DIR / "sess-old.jsonl").write_text(
json.dumps({"ts": 1, "session": "sess-old", "kind": "edit",
"summary": "from before"}) + "\n", encoding="utf-8")
events.load_disk_sessions()
before = {sid: m["label"] for sid, m in state.SESSIONS.items()}
self.restart()
after = {sid: m["label"] for sid, m in state.SESSIONS.items()}
self.assertEqual(before, after)
self.assertEqual(before["sess-piper"], "Piper · #45")
def test_an_agent_id_that_arrives_late_relabels_the_session(self):
"""Acceptance 6: events can precede the id; the label catches up,
and what it catches up to is what the restart reads back."""
self.launch("45-a-past-agent-140000", "Reed", "claude-sonnet-5")
self.ingest("sess-late", "session started", kind="session")
self.assertEqual(self.label("sess-late"), "You · sess-lat")
self.ingest("sess-late", "editing state.py", agent="45-a-past-agent-140000")
self.assertEqual(self.label("sess-late"), "Reed · #45")
self.restart()
self.assertEqual(self.label("sess-late"), "Reed · #45")
def test_a_name_the_board_never_saw_falls_back_to_agent_not_to_you(self):
"""Three states, three words: `Agent` when the id is known and the
name is not is the one thing `You` must never be said of."""
config.SESSIONS_DIR.mkdir(parents=True)
(config.SESSIONS_DIR / "sess-nameless.jsonl").write_text(
json.dumps({"ts": 1, "session": "sess-nameless", "kind": "edit",
"summary": "worked"}) + "\n", encoding="utf-8")
state.persist_identity("sess-nameless", {
"agentId": "45-a-past-agent-150000", "name": None,
"model": None, "task": TASK})
events.load_disk_sessions()
self.assertEqual(self.label("sess-nameless"), "Agent · #45")
def test_a_review_agent_without_a_name_keeps_saying_review(self):
config.SESSIONS_DIR.mkdir(parents=True)
(config.SESSIONS_DIR / "sess-rev.jsonl").write_text(
json.dumps({"ts": 1, "session": "sess-rev", "kind": "read",
"summary": "read the card"}) + "\n", encoding="utf-8")
state.persist_identity("sess-rev", {
"agentId": "review-45-a-past-agent-160000", "name": None,
"model": None, "task": TASK})
events.load_disk_sessions()
self.assertEqual(self.label("sess-rev"), "Review · #45")
# ── the file itself ───────────────────────────────────────────────────
def test_the_identity_is_a_file_of_its_own_beside_the_log(self):
"""The risk the card names: the logs are append-only JSONL whose
first line every reader takes for an event. Identity must not be a
header line, and must not be read back as a session of its own."""
self.launch("45-a-past-agent-170000", "Wren", "claude-opus-4-8")
self.ingest("sess-wren", "editing", agent="45-a-past-agent-170000")
log = config.SESSIONS_DIR / "sess-wren.jsonl"
first = json.loads(log.read_text(encoding="utf-8").splitlines()[0])
self.assertEqual(first["kind"], "edit")
self.assertEqual(
json.loads((config.SESSIONS_DIR / "sess-wren.who.json")
.read_text(encoding="utf-8")),
{"agentId": "45-a-past-agent-170000", "name": "Wren",
"model": "claude-opus-4-8", "task": TASK})
self.restart()
self.assertEqual(list(state.SESSIONS), ["sess-wren"])
def test_a_corrupt_identity_file_reads_as_unknown_not_as_a_crash(self):
config.SESSIONS_DIR.mkdir(parents=True)
(config.SESSIONS_DIR / "sess-bad.jsonl").write_text(
json.dumps({"ts": 1, "session": "sess-bad", "kind": "edit",
"summary": "worked"}) + "\n", encoding="utf-8")
(config.SESSIONS_DIR / "sess-bad.who.json").write_text(
"half a fi", encoding="utf-8")
events.load_disk_sessions()
self.assertEqual(self.label("sess-bad"), "Session · sess-bad")
def test_the_sidecar_is_rewritten_only_when_what_we_know_changes(self):
"""It is written whole on every change, so a session that says the
same thing a hundred times must not rewrite it a hundred times."""
self.launch("45-a-past-agent-180000", "Juno", None)
writes: list[tuple[str, dict]] = []
real = state.persist_identity
self.addCleanup(setattr, state, "persist_identity", real)
state.persist_identity = lambda sid, identity: (
writes.append((sid, identity)), real(sid, identity))[1]
self.ingest("sess-juno", "one") # you
self.ingest("sess-juno", "two", agent="45-a-past-agent-180000") # linked
for n in range(5):
self.ingest("sess-juno", f"more {n}", agent="45-a-past-agent-180000")
self.assertEqual([i["name"] for _, i in writes], [None, "Juno"])
def test_a_running_event_persists_nothing_at_all(self):
"""Running events are the live line, not history — they are not in
the log, so they must not conjure an identity file beside it."""
events.ingest_event({"v": 1, "session": "sess-live", "kind": "command",
"summary": "npm test", "running": True})
self.assertFalse((config.SESSIONS_DIR / "sess-live.who.json").exists())
self.assertFalse((config.SESSIONS_DIR / "sess-live.jsonl").exists())
# The page's own functions, run as the browser runs them — the chip on a
# replayed run is the visible half of this fix.
PARTS = (
r"const esc = \(s\) =>.*?\}\[c\]\)\);",
r"function agentFor\(sid\) \{.*?\n\}",
r"function shortModel\(model\) \{.*?\n\}",
r"function modelChip\(agent\) \{.*?\n\}",
)
class ReplayedChip(unittest.TestCase):
"""Acceptance 5: a replayed agent session wears its model chip — the
same way every time, from what was persisted rather than from what
happens to be in this board's memory."""
@classmethod
def setUpClass(cls):
cls.node = shutil.which("node")
if not cls.node:
raise unittest.SkipTest("node not available — chip behaviour unrun")
html = BOARD.read_text(encoding="utf-8")
out = []
for pattern in PARTS:
m = re.search(pattern, html, re.S)
if m is None:
raise AssertionError(f"board.html no longer defines {pattern!r}")
out.append(m.group(0))
cls.src = "\n".join(out)
def chip(self, sessions: list, agents: list, sid: str) -> str:
script = (f"const S = {{ state: {json.dumps({'sessions': sessions, 'agents': agents})} }};\n"
+ self.src
+ f"\nconsole.log(JSON.stringify(modelChip(agentFor({json.dumps(sid)}))));")
out = subprocess.run([self.node, "-e", script],
capture_output=True, text=True)
self.assertEqual(out.returncode, 0, out.stderr)
return json.loads(out.stdout)
def test_a_replayed_agent_session_wears_the_model_it_rode(self):
chip = self.chip(
[{"id": "s1", "agentId": "45-x-120000", "agentName": "Nell",
"agentModel": "claude-opus-4-8"}], [], "s1")
self.assertIn(">opus-4-8</span>", chip)
def test_a_replayed_session_that_inherited_the_default_still_says_nothing(self):
chip = self.chip(
[{"id": "s1", "agentId": "45-x-120000", "agentName": "Nell",
"agentModel": None}], [], "s1")
self.assertEqual(chip, "")
def test_the_persons_own_replayed_session_has_no_run_behind_it(self):
self.assertEqual(
self.chip([{"id": "s1", "agentId": None}], [], "s1"), "")
def test_a_live_record_still_wins_over_the_persisted_one(self):
"""The live record knows more (status, branch); the sidecar is the
fallback, not a second source of truth."""
chip = self.chip(
[{"id": "s1", "agentId": "45-x-120000", "agentName": "Nell",
"agentModel": "claude-opus-4-8"}],
[{"id": "45-x-120000", "session": "s1", "name": "Nell",
"model": "claude-sonnet-5", "status": "running"}], "s1")
self.assertIn(">sonnet-5</span>", chip)
if __name__ == "__main__":
unittest.main()