feat(desktop): surface managed-agent leadership and cooperative steal#1078
feat(desktop): surface managed-agent leadership and cooperative steal#1078wpfleger96 wants to merge 2 commits into
Conversation
Phase 3a emits a `leadership_status` observer frame per window-instance every 5s and handles a `claim_leadership` control frame. The desktop had no consumer. This adds the owner-side surface: a per-agent leader badge and a per-instance "Make leader" steal action. The frames already land in `eventsByAgent` via the owner-wide observer subscription, so leadership is a cached derivation rather than a new store. `getAgentLeadership` stays a stable map lookup (required by `useSyncExternalStore`); the `leadershipByAgent` array is rebuilt only when a leadership frame appends. Staleness stays out of the store — the row filters against a 5s clock so a crashed leader's badge drops within 15s without a new frame. The steal ack is non-authoritative: the UI converges off the stream, never optimistically flipping the badge. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Phase 3b — Leadership UI E2E screenshotsCaptured via Single-instance Leader badgeOne instance reporting Multi-instance freshest-leader badgeThree instances, one leader — the row badge reflects the freshest leader ( Leadership submenuThe "Make leader" cooperative-steal actionThe submenu open with a non-leader instance's "Make leader" entry hovered — the cooperative-steal entry point. |




Phase 3b: the desktop consumer for the cooperative leadership-steal feature shipped in the harness (Phase 3a, folded into #1062). Surfaces which window-instance leads each managed agent, and lets the owner trigger a cooperative steal.
Stack: #1062 → this PR
What this adds
ManagedAgentRowbeside the status — shows when an agent has a live leader instance. Hidden when no live instance reports....dropdown — lists each live instance (truncatedinstanceId, last-seen, leader marker) with a per-instance "Make leader" action. The current leader's item is disabled. Hidden when<= 1live instance, so the solo-dev UX is byte-unchanged.Design
The desktop is the owner/controller, not a contending instance.
leadership_statusframes already land ineventsByAgentvia the owner-wide observer subscription, so this is a cached derivation — no new store, no new subscription, no relay change.leadershipByAgentis a cachedMaprebuilt only when aleadership_statusframe appends, mirroringtranscriptByAgent.getAgentLeadershipstays a stable map lookup so it satisfies theuseSyncExternalStorereferential-stability contract (a fresh array pergetSnapshotwould render-storm).parseLeadershipPayloadnarrows the untrustedunknownpayload at the boundary; malformed frames are dropped.lastSeen = Date.parse(event.timestamp)withNaN→ drop.useNow(5000)clock at a 15s threshold (3 missed 5s ticks), so a crashed leader's badge ages out without a new frame arriving.max(lastSeen)among instances reportingisLeader, dissolving the<= 15stransient two-leader window after a crash without a "contested" state.claimManagedAgentLeadershipmirrorscancelManagedAgentTurnand returns{ status: "sent" }. The UI never optimistically flips — it converges off theleadership_statusstream, consistent with the harness race fix in 3a.Files
leadershipHelpers.ts(new) — pure derivation:parseLeadershipPayload,buildLeadership,filterStaleInstances,selectFreshestLeader.leadershipHelpers.test.mjs(new) — 17 behavior tests.observerRelayStore.ts—leadershipByAgentcached map, rebuild-on-append,getAgentLeadershipselector.agentControl.ts—claimManagedAgentLeadershipsender.useObserverEvents.ts—useAgentLeadershiphook.agentUi.ts—truncateInstanceId.ManagedAgentRow.tsx— badge + leadership submenu.