feat(relay): derive the two timestamps a client needs to triage a herd
herdr reports neither, so the relay owns both, per pane and on every `agents` and `panes` entry: last_active_at (the last status transition observed) and last_seen_at (the last time a client opened or drove the pane through here), in epoch ms because every client that will compare them is JavaScript. They answer the one thing a status cannot -- did this finish while I wasn't looking -- and they answer it as a comparison rather than a stored flag, so opening the pane clears it with no bookkeeping on either side. Ported from Collie's ActivityLedger (bridge/activity.ts), including the rules that are not obvious until they bite: - A first sighting seeds active_at == seen_at, so a fresh client never opens on a wall of alerts for work already handled at the desk. Only transitions observed after the relay first saw a pane may mark it unread -- the same rule the blocked-push path already follows. - The ledger keeps its own status memory rather than reading last_statuses, which the blocked-push logic owns and updates on its own schedule. - SEEN_ON is one chokepoint ahead of every handler, so a new handler cannot forget. `focus` is deliberately not in it: it moves herdr's cursor at the desk without the client reading anything. - Keyed by (host, pane_id), unlike the other pane maps -- every herdr numbers its own panes and this is the one that reaches disk, where a collision would stick. - Forgetting rides the existing stale sweep instead of a second reconcile policy beside it. That sweep already decides when a caller's picture is complete, and it covers shell panes, which a removal event derived from an agent status map never would. - Debounced 10s: an open pane's 3s mirror tick marks it seen every tick, free in memory and one write per tick forever on disk. Temp file plus rename, since a half file parses as nothing and silently costs everyone's unread column. Every field re-validated on load, including that True is not a timestamp -- it is an int in python and would sort a pane unread for good. Verified live: 30 panes stamped, nothing falsely unread on first connect, read_pane advanced seen_at within one poll, and activity.json holds 30 entries with the one pane I opened showing seen_at > active_at. Also fixes the utcnow() DeprecationWarning in audit(), which the new per-module tests turned from two journal lines per restart into a wall of test output. Same wire format -- `Z`, not `+00:00` -- and datetime.timezone.utc rather than datetime.UTC, which needs 3.11 against this file's requires-python of 3.10.
N
Nick007 committed
eba2a2a3dbb3c0c53d3f056a1b11e556faecd632
Parent: b0fa261