Skip to content

feat(desktop): surface managed-agent leadership and cooperative steal#1078

Draft
wpfleger96 wants to merge 2 commits into
duncan/leader-election-corefrom
duncan/leader-election-ui
Draft

feat(desktop): surface managed-agent leadership and cooperative steal#1078
wpfleger96 wants to merge 2 commits into
duncan/leader-election-corefrom
duncan/leader-election-ui

Conversation

@wpfleger96

Copy link
Copy Markdown
Collaborator

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

  • Leader badge in ManagedAgentRow beside the status — shows when an agent has a live leader instance. Hidden when no live instance reports.
  • Leadership submenu in the row's ... dropdown — lists each live instance (truncated instanceId, last-seen, leader marker) with a per-instance "Make leader" action. The current leader's item is disabled. Hidden when <= 1 live instance, so the solo-dev UX is byte-unchanged.

Design

The desktop is the owner/controller, not a contending instance. leadership_status frames already land in eventsByAgent via the owner-wide observer subscription, so this is a cached derivation — no new store, no new subscription, no relay change.

  • leadershipByAgent is a cached Map rebuilt only when a leadership_status frame appends, mirroring transcriptByAgent. getAgentLeadership stays a stable map lookup so it satisfies the useSyncExternalStore referential-stability contract (a fresh array per getSnapshot would render-storm).
  • parseLeadershipPayload narrows the untrusted unknown payload at the boundary; malformed frames are dropped. lastSeen = Date.parse(event.timestamp) with NaN → drop.
  • Staleness lives in the component, not the store — the store stays time-independent. The row filters against a useNow(5000) clock at a 15s threshold (3 missed 5s ticks), so a crashed leader's badge ages out without a new frame arriving.
  • Freshest-leader rule: the badge reflects max(lastSeen) among instances reporting isLeader, dissolving the <= 15s transient two-leader window after a crash without a "contested" state.
  • Non-authoritative ack: claimManagedAgentLeadership mirrors cancelManagedAgentTurn and returns { status: "sent" }. The UI never optimistically flips — it converges off the leadership_status stream, 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.tsleadershipByAgent cached map, rebuild-on-append, getAgentLeadership selector.
  • agentControl.tsclaimManagedAgentLeadership sender.
  • useObserverEvents.tsuseAgentLeadership hook.
  • agentUi.tstruncateInstanceId.
  • ManagedAgentRow.tsx — badge + leadership submenu.

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>
@wpfleger96 wpfleger96 marked this pull request as draft June 17, 2026 04:42
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96

Copy link
Copy Markdown
Collaborator Author

Phase 3b — Leadership UI E2E screenshots

Captured via leadership-screenshots.spec.ts (smoke project), driving the real cached-map consumer through the __BUZZ_E2E_SEED_LEADERSHIP__ seed hook — synthetic leadership_status frames routed through the production appendAgentEvent rebuild path, not a stub.

Single-instance Leader badge

One instance reporting isLeader: true — the row shows the crown "Leader" badge with no Leadership submenu (≤1 instance, nothing to steal).

01-single-instance-leader

Multi-instance freshest-leader badge

Three instances, one leader — the row badge reflects the freshest leader (max(lastSeen) among isLeader: true).

02-multi-instance-badge

Leadership submenu

The ... dropdown's Leadership submenu listing each instance: truncated instanceId + last-seen + leader marker, with the current leader's item disabled.

03-leadership-submenu

"Make leader" cooperative-steal action

The submenu open with a non-leader instance's "Make leader" entry hovered — the cooperative-steal entry point.

04-make-leader-action

wpfleger96 pushed a commit that referenced this pull request Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant