tasks: card 45 — a past agent session is labelled You

The Sessions list shows agent names only for runs this board process
launched; anything replayed from disk loses its agent id and falls
through to the You branch of session_label(). Diagnosis and evidence in
the card.
This commit is contained in:
istos
2026-07-31 16:17:21 +02:00
parent faba0fd5a4
commit b2c009e1b8
@@ -0,0 +1,142 @@
# 45 — A past agent session is labelled "You", because "You" is what the board says when it does not know
**Status:** Backlog
**Priority:** High — the flight recorder misattributes work, and
misattribution is worse than an absence: the list looks complete
**Type:** Bug
Sessions shows agent names only for agents this board process launched.
Every session read back from disk loses its agent id, and the label falls
through to `You · <id>` — so past agent runs do not vanish from the list,
they appear as the human's own sessions. What looks like "several sessions
of me and no old agents" is one bug: the old agents *are* the several
sessions of me.
## Context
The label is decided in one place, `manager/core/events.py:27`:
```python
def session_label(meta: dict) -> str:
agent_id = meta.get("agentId") or ""
if agent_id:
record = state.AGENTS.get(agent_id) or {}
...
return f"{who} · #{num.group(1)}" if num else who
return f"You · {meta['id'][:8]}"
```
**"You" is not a positive identification. It is the `else`.** Anything the
board cannot attribute to an agent it attributes to the person.
That is fine while the board is up. `ingest_event` (`:41`) reads
`raw["agent"]` — the adapter puts `BOARD_AGENT_ID` there
(`adapters/claude/emit.py:194`, from the child env set in
`agents.py:138`) — and stores it on the session meta, so a live run reads
`Reed`, `Juno`, `Basil`.
It breaks at the disk boundary, in two steps:
1. **The agent id is never persisted.** The `event` dict built in
`ingest_event` (`:47-59`) carries `ts`, `session`, `kind`, `summary`
and optionally `file` / `cmd` / `detail` / `ok`. The agent id is read
into the *session registry* and never copied into the *event*, and the
event is what `state.persist()` writes. Verified on this repo's own
state: every persisted event's keys are
`['kind', 'session', 'summary', 'ts']`, and of 63 files in
`manager/local/state/sessions/`, the only one containing the string
`"agent"` is `board.jsonl` — a different log.
2. **So the replay cannot recover it.** `load_disk_sessions()` (`:96`)
rebuilds each session's meta with `"agentId": None` hardcoded. It is
not an oversight in that function — the data is not on disk to read.
`session_label()` then takes the `else`.
The evidence, from this checkout while the card was written. Two rows in
Sessions read `You`, and their last persisted event is:
```
be5408af… kind: report "Nell's report on 41-the-drawer-renders-…"
d1a65161… kind: report "Piper's report on 35-the-site-reads-on-a-phone.md"
```
Nell and Piper are agents. Their sessions are wearing the user's label,
and carrying their own closing reports underneath it.
A third thing compounds it: `state.AGENTS` is memory-only
(`state.py:22`), so even with the id restored, the *name* would be gone
after a restart — the best today's fallback could say is `Agent · #41`.
The same gap is why a replayed session wears no model chip
(`board.html:1575` resolves it through the live agent list).
**Affected areas:** `manager/core/events.py` (event shape, replay,
labelling) and `manager/core/state.py` (what a session's identity is and
where it lives).
## What to build
- **Persist who a session belonged to.** The smallest fix is one more key
on the persisted event; the better one is a per-session identity record
written once when the session is first linked — `agentId`, the agent's
name, its model, its task — because the name and model are the parts a
restart loses and neither can be recovered from an event stream.
- **Read it back in `load_disk_sessions()`**, so a replayed session
arrives with the same label it had while it ran.
- **Stop asserting "You" when the board does not know.** After the fix,
absence of an agent id on a *live* session still means the person — a
human Claude Code session genuinely sends none. For a session replayed
from an old file that predates persistence, absence means unknown, and
the label should say something neutral rather than claim it was you.
Old logs must not be retro-attributed, in either direction.
- **Keep the fallbacks honest and distinct.** `Agent` when the id is
known and the name is not; the name when the name was persisted;
neutral when nothing was recorded. Three different states, three
different words.
- **Let the model chip follow.** If the model is persisted with the
identity, a replayed session can wear its chip instead of the board
saying nothing rather than guessing — that rule was written for a board
that had nothing to read.
**Out of scope** — tempting neighbours left alone:
- Making `state.AGENTS` a durable registry of every run ever launched.
This card needs a session to remember who it was, not the board to
remember every agent.
- The Sessions list's ordering, filters or timeline rendering.
- `board.jsonl` and the board event log, which are a separate stream.
- Retro-labelling the sessions already on disk. They have no id to
recover; a migration that guesses from `"<Name>'s report on …"` would
be a heuristic in the one place that should not have one.
## Acceptance
- [ ] Given an agent session that ran before the current board process,
when Sessions is opened, then the row carries the agent's name and
task — not `You`.
- [ ] Given a human session, when it is replayed from disk, then it still
reads `You` — this fix does not relabel the person's own sessions.
- [ ] Given a session file written before this change (no identity
recorded), when it loads, then it is labelled neutrally and no row
claims to be you.
- [ ] Restarting the board does not change any session's label.
- [ ] A replayed agent session shows its model chip, or the board keeps
saying nothing — but the same way every time, not by accident of
what happens to be in memory.
- [ ] Edge case: a session whose events arrived before its agent id did
(the id is attached on a later event) is labelled correctly after
the fact, as it is today.
## Notes
The design principle this breaks is one the project states elsewhere: the
board says nothing rather than guessing — that is why a session with no
known model wears no chip. Labelling an unattributable session `You` is
the opposite of that rule, and it is load-bearing here because the
Sessions view is a flight recorder. A gap in it is obvious; a confident
wrong name is not.
**Risks** — session files are append-only JSONL read by
`load_disk_sessions()` with a bare `json.JSONDecodeError` guard. Whatever
shape the identity takes, a file written by the old code and a file
written by the new one both have to load; a header line prepended to
existing files would break every reader that assumes line one is an
event.