feat(agents): add active turn indicators to Agents Menu#1005
Merged
Conversation
Emit turn_completed observer event from the ACP harness on successful and cancelled turn exits (symmetric with existing turn_error/agent_panic). Add a desktop-side derived store that tracks active turns per agent per channel by watching the observer event stream, with a 5-minute staleness timeout as safety net. Surface per-channel 'Working in #channel' badges and a pulsing 'Working' status badge in the Agents Menu. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…tionGuard The previous implementation emitted turn_completed only on the Ok and Cancelled paths in lib.rs, leaving 8+ error/timeout exit paths in run_prompt_task uncovered. A missed completion event causes phantom "Working" indicators until the staleness timeout fires. Replace with a TurnCompletionGuard scope guard in pool.rs that captures the observer handle and turn metadata at creation time and emits turn_completed on Drop — covering every exit path including panics. Remove the now-redundant manual emits from lib.rs. Rewrite activeAgentTurnsStore.ts to match the revised spec: - Track turns by turnId (not channelId) for correct multi-channel handling - Activity-based staleness: mark stale after 20s of no acp_read/write, remove entirely after 90s - Cap at 4 concurrent turns per agent (matches pool size) - Add useActiveAgentTurnsDetailed hook returning Map with stale flag - Keep useActiveAgentTurns returning Set<string> for backward compat Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
getActiveChannelsForAgent and getActiveTurnsForAgent returned new object instances on every call, breaking useSyncExternalStore's reference equality check and causing excessive re-renders on every observer event. Cache Set/Map per agent key, invalidated only when turns actually change (startTurn, endTurn, pruneExpired). Also respects prefers-reduced-motion for pulsing badges and documents the sorted-input invariant on processEvent. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Captures 3 states: baseline idle, single agent working, and mixed states (one agent working in 2 channels). Adds __BUZZ_E2E_SEED_ACTIVE_TURNS__ hook to e2eBridge with monotonic seq counter to prevent same-millisecond event deduplication when seeding multiple turns. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Fix seq-restart bug: detect when agent restarts (seq resets to 1) and
reset the high-water mark so subsequent events are processed instead of
silently dropped.
Remove dead code: ActiveTurnInfo type, useActiveAgentTurnsDetailed hook,
getActiveTurnsForAgent, cachedDetailedMaps, STALE_AFTER_MS, and the
staleness-related prune logic (~60 lines with zero consumers).
Fix badge text from "Working in # general" to "Working in #general".
Change TurnCompletionGuard Drop payload from {"outcome": "dropped"} to
{} — the guard cannot know the actual outcome on the drop path.
Remove disarm() method and its #[allow(dead_code)] annotation.
Add unit tests covering seq filtering, restart regression detection,
eviction at MAX_TURNS_PER_AGENT, and turnId-vs-channelId endTurn
fallback.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Working in #channel and static # channel membership badges now navigate
to the channel when clicked. Both badge types get cursor-pointer and
hover:opacity-80 styling to indicate interactivity.
channelsByPubkey changed from Record<string, string[]> to
Record<string, {id, name}[]> so badge click handlers have the channel
ID needed for goChannel navigation.
Co-authored-by: Will Pfleger <wpfleger@squareup.com>
Signed-off-by: Will Pfleger <wpfleger@squareup.com>
The seq-only watermark relied on a seq===1 heuristic to detect agent restarts. That heuristic is fragile: it assumes the harness always restarts at seq 1 and can misfire on legitimate seq-1 events. Replace it with a composite (timestamp, seq) watermark reusing the existing compareObserverEvents comparator. Post-restart streams are now handled for free because wall-clock timestamp keeps climbing across restarts, and full-buffer replays are idempotent. Eviction (turn_completed / turn_error / agent_panic) stays unconditional so a stale replayed start can never resurrect an already-evicted turn; the watermark gates only new-turn creation and activity refresh. Co-authored-by: Will Pfleger <wpfleger@squareup.com> Signed-off-by: Will Pfleger <wpfleger@squareup.com>
wpfleger96
pushed a commit
that referenced
this pull request
Jun 12, 2026
Collaborator
Author
The store gated starts on the (timestamp, seq) watermark but processed turn_completed/turn_error/agent_panic unconditionally. The harness emits turn_error and agent_panic with a null turnId, so eviction falls back to deleting the first turn matching the channel. On every observer event the full buffer is replayed, so a stale eviction re-ran the channel-match fallback and deleted the live turn — whose start sits below the watermark and never returns. Working badges for that channel went permanently dead after one error. Gating every event kind uniformly makes replays a no-op given the sorted- buffer invariant. Resurrection was never the risk the old comment claimed: it would require reprocessing a stale start, which the watermark blocks. Co-authored-by: Will Pfleger <wpfleger@squareup.com> Signed-off-by: Will Pfleger <wpfleger@squareup.com>
The relay-agent pill map fell back to the channel name as the id when channelIds and channels arrays misaligned, silently producing a pill that navigated to a name as if it were a channel id. Drop the entry instead so navigation never resolves to a bogus target. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96
added a commit
that referenced
this pull request
Jun 12, 2026
…session-new * origin/main: Add relay disconnect UX: friendly errors, reconnect, cached identity (#1004) feat(agents): add active turn indicators to Agents Menu (#1005) ci: add fork guards to docker, release, and auto-tag workflows (#1007) docs(nip-rs): add optional thread read context scheme (#1006)
tellaho
added a commit
that referenced
this pull request
Jun 12, 2026
…tate * origin/main: Add relay disconnect UX: friendly errors, reconnect, cached identity (#1004) feat(agents): add active turn indicators to Agents Menu (#1005) ci: add fork guards to docker, release, and auto-tag workflows (#1007) docs(nip-rs): add optional thread read context scheme (#1006) fix(huddle): Pocket TTS quality overhaul — reference parity + cross-message pipelining (#997) Add manual ACP session rotation command (#932) fix(desktop): heal stale persona_team_dir paths in release builds (#1003) ci(docker): publish public ghcr.io/block/buzz image (native multi-arch) (#986) fix(buzz-agent): cap tool-result text at 50 KiB with middle elision (#952) feat(huddle): sentence-at-a-time voice-mode guidelines for lower TTS latency (#996) Shard desktop Playwright CI jobs (#992) chore(release): release version 0.3.18 (#995) Video Player Improvements (#993) Improve first-run welcome setup (#970) fix(release): use legacy updater key secret (#991) Replace built-in personas with Fizz (#987)
tlongwell-block
pushed a commit
that referenced
this pull request
Jun 13, 2026
* origin/main: (33 commits) fix(desktop): make Windows release compile cleanly (#1029) Add production Docker Compose bundle (#985) feat(profile): show active turn badges on agent profile panel and popover (#1026) chore(release): release version 0.3.20 (#1027) fix(release): resolve Windows sidecar path and Linux AppImage updater format (#1024) chore(release): release version 0.3.19 (#1014) fix(release): ignore prerelease tags in changelog generation (#1021) fix: repair main build after cross-PR merge skew (#1020) feat(agents): show per-turn duration and prune dead turns within ~25s of host crash (#1017) fix(release): replace hermit with native tool setup on Windows job (#1018) feat(acp): surface error-class outcomes to the activity feed only, never the channel (#1010) fix(desktop): migrate Sprout workspace storage (#1016) feat(auth): force token refresh on rejected token (401/403), never the browser (#1015) fix(release): mark prerelease versions so they do not become latest (#1013) feat(acp): implement systemPrompt with protocol version gating (#981) fix(release): update repository name check from block/sprout to block/buzz (#1012) feat(release): all-OS desktop builds + universal auto-update manifest (#1011) Add relay disconnect UX: friendly errors, reconnect, cached identity (#1004) feat(agents): add active turn indicators to Agents Menu (#1005) ci: add fork guards to docker, release, and auto-tag workflows (#1007) ... Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Surface per-channel "Working in #channel" indicators in the Agents Menu so users can see at a glance which agents are actively working and where — without navigating to each channel individually.
Problem
The Agents Menu shows process state (running/stopped) and presence (online/offline), but not cognitive state. Users cannot tell whether an agent is in the middle of a turn without opening each channel and checking the activity panel.
Solution
Two-layer approach:
Backend (ACP harness): A
TurnCompletionGuardscope guard inrun_prompt_task()emitsturn_completedon every exit path (success, error, timeout, cancel, panic) via itsDropimpl. This is symmetric with the existingturn_startedemit at the top of the function — every turn that starts is guaranteed to complete in the observer stream. TheDroppayload is{}because the guard cannot know the actual outcome on the drop path.Desktop (derived store + UI):
activeAgentTurnsStore.ts— subscribes to the observer relay store and maintains aMap<agentPubkey, Map<turnId, ActiveTurn>>of active turns. Turns become active onturn_startedand inactive onturn_completed/turn_error/agent_panic. A 90-second inactivity timeout prunes turns whose completion event was missed (checked every 5s).AgentStatusBadgegains a pulsing "Working" variant that takes priority over "running".ManagedAgentRowrenders "Working in #channel-name" badges (max 3) below channel membership badges.Event ordering: composite watermark
The store dedups observer events with a composite
(timestamp, seq)watermark per agent, reusing thecompareObserverEventscomparator fromobserverRelayStore. Every event kind is gated on the watermark uniformly — an event is processed only if it is strictly newer than the last one seen.turn_completed/turn_error/agent_panic) all skip equal-or-older events, so full-buffer replays are a complete no-op given the sorted-buffer invariant.seqto 1 on restart, but wall-clocktimestampkeeps climbing, so the comparator accepts post-restart events without anyseq === 1special case.turn_error/agent_panicwith a nullturnId, so the store falls back to deleting the first turn matching the channel. Processing such an eviction unconditionally on replay would re-run that channel-match fallback and delete the live turn whose start event is below the watermark. Gating evictions on the watermark closes that hole. Resurrection is not a risk: it would require reprocessing a stale start, which the watermark already blocks.Exported API
useActiveAgentTurns(pubkey)Set<channelId>where agent has active turnsuseActiveAgentTurnsBridge(agents)getActiveChannelsForAgent(pubkey)subscribeActiveAgentTurns(listener)syncAgentTurnsFromEvents(pubkey, events)resetActiveAgentTurnsStore()Design decisions
TurnCompletionGuardcovers all exit paths structurally (including panics) without requiring each error handler to remember to emit.acp_read/acp_write) refresheslastActivityAtonly for the matchingturnId, so unrelated agent activity does not prevent expiry.endTurnchannelId fallback: Ifturn_completedarrives without aturnId(legacy path), falls back to removing the first turn matching thechannelId.goChannel(id).stopPropagation()prevents triggering the parent row expand.Replaces #994 (which was opened from a fork); identical change set, head on
block/buzz.