Skip to content

fix(desktop): sync derived active-turn liveness in channel activity path#1492

Merged
tellaho merged 4 commits into
mainfrom
tho/active-turn-bridge-fix
Jul 7, 2026
Merged

fix(desktop): sync derived active-turn liveness in channel activity path#1492
tellaho merged 4 commits into
mainfrom
tho/active-turn-bridge-fix

Conversation

@tellaho

@tellaho tellaho commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Overview

Category: fix
User Impact: The working/liveness indicator in a channel's agent Activity panel now clears as soon as the agent's turn completes, instead of continuing to spin after the work is done.

Problem: In the channel Activity panel, a raw turn_completed observer event could be visible in the feed while the liveness indicator kept spinning. The channel screen mounted the observer bridge (which feeds raw ACP frames into the activity panel) but never mounted the active-turns bridge, so the derived liveness store went stale — the exact "raw event arrived but the non-raw feed didn't update" mismatch. Nearby surfaces (profile panel, agents page, sidebar) already mount both bridges and were unaffected.

Solution: Mount useActiveAgentTurnsBridge right next to the existing observer bridge in ChannelScreen, on the same agent list, so channel-activity derived liveness consumes the same observer events the raw rail does. The bridge's sync loop is extracted into an exported syncActiveAgentTurnsFromObserver so a regression test can drive the real observer-ingestion path without a React renderer. Also wires resetActiveAgentTurnsStore() into the workspace-switch reset, closing a singleton-leak gap per the workspace-switching contract in AGENTS.md.

File changes

desktop/src/features/channels/ui/ChannelScreen.tsx
Mount useActiveAgentTurnsBridge(observerBridgeAgents) alongside the existing useManagedAgentObserverBridge so the derived active-turns store stays in sync with raw observer frames in the channel activity path. The store dedupes per-agent via a watermark, so double-mounting with other surfaces is a no-op.

desktop/src/features/agents/activeAgentTurnsStore.ts
Extract the bridge's sync loop into exported syncActiveAgentTurnsFromObserver(agents); the hook now delegates to it. No behavior change — enables the regression to exercise the exact production path. Also resetActiveAgentTurnsStore() is now invoked on workspace switch.

desktop/src/features/agents/activeAgentTurnsStore.test.mjs
New regression "observer → active-turns bridge sync": injects turn_started then turn_completed through the real observer ingestion path and asserts the raw feed retains the completion event while derived liveness clears. Plus a case asserting non-running/deployed agents don't populate the store.

desktop/src/features/workspaces/useWorkspaceInit.ts
Add resetActiveAgentTurnsStore() to resetWorkspaceState() — the active-turns store is a module-level singleton holding workspace-scoped state and previously leaked across workspace switches.

desktop/src/features/agents/ui/ManagedAgentSessionPanel.tsx
Net-zero change from the temporary debug harness used during diagnosis (added and fully removed within this branch); only minor prop-ordering churn remains.

Reproduction Steps

  1. Run the desktop app with a local managed agent and open a channel the agent is in.
  2. Mention the agent so it starts a turn, then open its Activity panel from the channel (bot activity bar or member menu).
  3. Watch the turn finish: previously the liveness indicator kept spinning after turn_completed appeared in the raw feed; now it clears promptly.
  4. Automated: cd desktop && pnpm test — see the "observer → active-turns bridge sync" regression in activeAgentTurnsStore.test.mjs (1551/1551 pass).

No visual/markup changes — this fixes the state plumbing the liveness UI depends on.

tellaho added a commit that referenced this pull request Jul 6, 2026
Rebased from tho/active-turn-bridge-fix (#1492) onto post-#1380 main,
squashing the temporary debug-harness add/remove churn that netted to
zero. Behavior preserved:

- ChannelScreen mounts useActiveAgentTurnsBridge next to the existing
  observer bridge so raw observer frames (e.g. turn_completed) and the
  derived liveness store stay in sync in the channel activity path
- Extract syncActiveAgentTurnsFromObserver() from the bridge hook so the
  regression test drives the exact observer→derived-liveness path
- resetActiveAgentTurnsStore() wired into workspace-switch reset per the
  workspace-switching singleton contract
- Regression coverage: observer→active-turns bridge sync keeps the raw
  feed's completion event while derived liveness clears; non-running
  agents don't populate the store

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho force-pushed the tho/active-turn-bridge-fix branch from f5edf42 to f9ffb01 Compare July 6, 2026 16:51
tellaho added a commit that referenced this pull request Jul 6, 2026
Rebased from tho/active-turn-bridge-fix (#1492) onto post-#1380 main,
squashing the temporary debug-harness add/remove churn that netted to
zero. Behavior preserved:

- ChannelScreen mounts useActiveAgentTurnsBridge next to the existing
  observer bridge so raw observer frames (e.g. turn_completed) and the
  derived liveness store stay in sync in the channel activity path
- Extract syncActiveAgentTurnsFromObserver() from the bridge hook so the
  regression test drives the exact observer→derived-liveness path
- resetActiveAgentTurnsStore() wired into workspace-switch reset per the
  workspace-switching singleton contract
- Regression coverage: observer→active-turns bridge sync keeps the raw
  feed's completion event while derived liveness clears; non-running
  agents don't populate the store

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho force-pushed the tho/active-turn-bridge-fix branch from f9ffb01 to 22feb20 Compare July 6, 2026 17:09
Rebased from tho/active-turn-bridge-fix (#1492) onto post-#1380 main,
squashing the temporary debug-harness add/remove churn that netted to
zero. Behavior preserved:

- ChannelScreen mounts useActiveAgentTurnsBridge next to the existing
  observer bridge so raw observer frames (e.g. turn_completed) and the
  derived liveness store stay in sync in the channel activity path
- Extract syncActiveAgentTurnsFromObserver() from the bridge hook so the
  regression test drives the exact observer→derived-liveness path
- resetActiveAgentTurnsStore() wired into workspace-switch reset per the
  workspace-switching singleton contract
- Regression coverage: observer→active-turns bridge sync keeps the raw
  feed's completion event while derived liveness clears; non-running
  agents don't populate the store

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho force-pushed the tho/active-turn-bridge-fix branch from 22feb20 to 8d0becf Compare July 6, 2026 18:08
@tellaho tellaho requested a review from wpfleger96 July 6, 2026 19:49
@tellaho tellaho marked this pull request as ready for review July 6, 2026 19:49
}, [messagesQuery.data]);
// No `lastMessageAt` fallback: it is reply-inclusive and would clear unread
// thread/sidebar state before a real top-level position is known.
const activeReadAt = latestActiveMessage

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Nit: the comment removed just above this line was load-bearing context.

The deleted two lines explained why lastMessageAt is not used as an activeReadAt fallback — it's reply-inclusive and would clear unread thread/sidebar state before a real top-level position is known. The logic is unchanged and correct, but that constraint isn't obvious from the surrounding code. Suggest restoring the comment so the null fallback stays self-documenting.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated by Ned 🤖 in bb069eb.

Restores the comment explaining why lastMessageAt is not used as an
activeReadAt fallback: it is reply-inclusive and would clear unread
thread/sidebar state before a real top-level position is known.
Behavior unchanged.

ChannelScreen.tsx sat at 999/1000 lines, so the two restored comment
lines trip the file-size guard; adds a narrowly scoped 1002-line
override entry noting the comment-only overage.

Addresses review feedback on #1492.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
tellaho and others added 2 commits July 6, 2026 20:36
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w <52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho enabled auto-merge (squash) July 7, 2026 03:52
@tellaho tellaho merged commit 1f5ba5b into main Jul 7, 2026
25 checks passed
@tellaho tellaho deleted the tho/active-turn-bridge-fix branch July 7, 2026 03:59
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.

2 participants