fix: usage tracker and agent names follow the agent, not its sidebar slot#53
fix: usage tracker and agent names follow the agent, not its sidebar slot#53AliHamzaAzam wants to merge 7 commits into
Conversation
The overlay re-paired transcripts to tmux windows every tick by activity rank, so two agents swapping turns swapped their windows: names, panes, and usage accounts jumped between sidebar rows. Pair by a sticky binding stamped on the window (@repomon_session) instead; the old oldest-with-oldest heuristic now runs only on first contact, ordered by window id so a recycled slot name cannot steal a survivor's window. Bindings live in tmux, so they survive daemon restarts and die with their window. Bound-but-quiet transcripts are also protected from live-count truncation: their window proves the agent alive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The in-lane agent cursor was a raw index into agent_sessions, which the daemon re-sorts newest-active-first on every poll; the index was only re-derived on lane change, so sitting on a lane let the cursor drift onto whichever agent occupied that slot. The usage corner then showed the wrong account's limits and keys routed to the wrong pane. Pin the cursor to a durable SessionRef and re-resolve the index every sync tick. Deliberate moves (Tab, notification jumps, spawn focus, expanded rows) all funnel through the pin; an unanchored lane pins on arrival; a missing agent is tolerated for a few refreshes (overlay flap) before the pick adopts the occupant.
Slot names like lane-7-2 are recycled when an agent exits and another spawns, so a remembered Window ref (session memory, cursor anchor) could resolve to a brand-new different agent. Transcript UUIDs are never reused; the window remains the fallback for just-spawned placeholders that have no .jsonl yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A multi-agent review of the sticky-pairing change confirmed lifecycle holes, all fixed here with tests: - Only stamp @repomon_session for transcripts that are actively writing: a quiet transcript standing in during the spawn race (the .jsonl appears a beat after the window) is displayed but not bound, so the guess can no longer wedge for hours. - Supersession: /clear and fork-on-resume rotate the session id inside a window; when fresh unclaimed transcripts outnumber free windows, the coldest stale bindings are released and re-stamped to the live transcripts. Idle fleets are never shuffled. - select_kept_summaries keeps the fresh backstop: an actively-writing transcript can no longer be truncated in favor of a stale bound one. - Stamp by window id, not name (a slot recycled mid-tick cannot inherit the old binding), skip stamping on ticks that ran on the last-good window snapshot, and treat set-option's "no such window" as the benign absence it is. - agent.adopt stamps the resumed session id on its window itself, at the one moment the mapping is ground truth. - TUI: expanded-mode rows compare agent identity (not raw index) before wiping scrollback, and the spawn-focus give-up clock counts data refreshes rather than event-loop iterations.
Coldest-first picked the wrong window when several bound agents were quiet: the transcript that stopped writing most recently is the one that just rotated (/clear) into the newcomer, while a long-cold one is simply an idle agent whose window must not be given away. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Live incident: first contact after a daemon restart had to pair three windows against several simultaneously-plausible transcripts, guessed two of them wrong by activity rank, and the sticky stamps froze the mistake - the user's agent names pointed at the wrong panes and a live session ranked too old to even be listed. Activity rank cannot see which conversation is physically inside which pane, so it no longer decides anything durable. Pass 2 now only nominates candidates; the stamp task captures the lane's unclaimed panes and stamps each transcript onto the single pane that visibly contains its last message (whitespace/markdown-insensitive fingerprint). No match, or an ambiguous one, just defers to a later tick - the pairing stamps itself the moment the agent's turn is on screen. The rank-based zip remains as the display-only hint. Verified end-to-end with a deliberately crossed activity order: the old code stamps the swap; the gate stamps the truth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Follow-up commit 4269d7e after a live recurrence: first contact after a daemon restart guessed two of three windows wrong by activity rank (several transcripts fresh at once), and the sticky stamps froze the mistake. Stamps are now gated on pane evidence — a transcript only binds to the window whose pane visibly contains its last message (normalized fingerprint, unique match required). The activity-rank zip remains display-only. Verified e2e with a deliberately crossed activity order: the old code stamps the swap, the gate stamps the truth. The wrong live bindings on the affected lane were repaired in place. 🤖 Generated with Claude Code |
The TODAY section was capped at 12 commits and the page's scroll position was derived purely from the selected lane, so commits past the fold were unreachable. The wheel and PgUp/PgDn now scroll the whole Fleet page freely (cap removed); any deliberate selection move (arrows, click, jump) snaps the view back to following the selection.
|
Also rode along on this branch: 86d6eab adds free page scrolling to the Fleet view (wheel / PgUp/PgDn) and removes the 12-commit TODAY cap — commits past the fold were unreachable. Arrow keys, clicks, and jumps snap the view back to following the selection. |
Problem
Switching between agents showed agent A's usage limits while agent B was selected, and user-assigned agent names appeared to jump between agents whenever sidebar positions shifted.
Two independent root causes:
session_idxindexeslane.agent_sessions, which the daemon re-sorts newest-activity-first on every poll; the index was only re-derived by identity on lane change, so sitting on a lane let the cursor silently drift onto whichever agent occupied that slot. The usage corner (focused_account_key) and key/capture routing (selected_window) then targeted the wrong agent.tmux_windowre-bound to the other window, moving names, panes, and usage accounts between rows. Labels themselves were always correctly keyed by transcript id; the pairing moved the rows out from under them.Fix
Daemon: sticky transcript-window pairing. Pairing is anchored by a
@repomon_sessiontmux window option: stamped once at first contact, honored on every subsequent tick, read back at zero cost through the existinglist-windowsprobe. Bindings live in tmux, so they survive daemon restarts and die with their window (slot-name recycling handled for free). First-contact heuristics run once, ordered by window id (never reused) rather than recyclable slot names.Lifecycle hardening (from an adversarial multi-agent review of the initial design):
.jsonlappears a beat after the window) is displayed but never bound, so a guess can't wedge./clearand fork-on-resume rotate the session id inside a window; when fresh unclaimed transcripts outnumber free windows, the warmest stale binding (the one that just went quiet, i.e. the rotated predecessor) is released and re-stamped to the live transcript. Idle fleets are never shuffled.agent.adoptstamps its resumed session id itself — ground truth instead of a guess.TUI: identity-anchored session cursor. The cursor is pinned to a durable
SessionRef(transcript id first — UUIDs are never reused; window name only for placeholders) and re-resolved every sync tick. Deliberate moves (Tab, notification jumps, spawn focus, expanded rows) funnel through the pin; an unanchored lane pins on arrival; a missing agent is tolerated for a few refreshes (overlay flap) before adopting the occupant. Expanded-mode rows compare identity, not raw index, before wiping scrollback, and the spawn-focus give-up clock counts data refreshes rather than event-loop iterations.Verification
/clearrotation (the rotated window re-stamped to the continuation; the idle agent untouched).No wire changes:
AgentSessionfields and lane.list ordering are untouched, so the iOS companion and older TUIs are unaffected.🤖 Generated with Claude Code