Skip to content

fix: keep agent activity visible in thread composer and after mobile resume - #13

Merged
cursor[bot] merged 4 commits into
mainfrom
devin/1785562561-fix-agent-activity-visibility
Aug 1, 2026
Merged

fix: keep agent activity visible in thread composer and after mobile resume#13
cursor[bot] merged 4 commits into
mainfrom
devin/1785562561-fix-agent-activity-visibility

Conversation

@devin-ai-integration

Copy link
Copy Markdown

Summary

Two independent "the agent is running but the UI says nothing" bugs.

Desktop — thread composer showed typing only. The channel composer resolves activity through the unified working signal (observer-derived turns primary, typing fallback), but the thread composer was wired to typing entries alone:

-workingBotPubkeys={threadComposerBotTypingPubkeys}
+workingBotPubkeys={threadComposerWorkingBotPubkeys}  // observer turns ∪ thread typing

So while an agent worked in a thread, the active turn sat in activeAgentTurnsStore and the channel view knew about it, but the bar under the thread composer went blank as soon as typing TTL lapsed.

The store already exposes getActiveAgentsForConversation(), keyed by the turn's conversationId. The thread panel only knows the NIP-10 root event ID, so this adds deriveAgentConversationId() — a TS port of buzz-acp's derivation:

sha256("buzz-acp-conversation-v1" ‖ channelUuidBytes(16) ‖ rootEventIdHexAscii)[0..16] → UUID

Raw bytes, no version/variant fixup — matching crates/buzz-acp/src/conversation.rs. Test vectors are taken from the Rust implementation, not hand-computed. deriveAgentConversationIdOrNull() returns null for malformed input (e.g. an optimistic optimistic-<uuid> root that has no relay event ID yet) so the render path can't throw.

Mobile — resume trusted a dead socket. onAppPaused() only schedules the disconnect 5s out; a suspended isolate never runs that timer, so the session stayed connected, and onAppResumed() early-returned on state.status == connected — no reconnect, no REQ replay. Combined with iOS/Android silently invalidating the socket in the background, the app sat on a half-open connection until force-killed.

  • Resume now skips the reconnect only for a genuinely short switch: backgrounded < 5s and socket connected and a recent inbound frame. Otherwise it replaces the socket and replays live subscriptions.
  • The socket had no liveness check at all. Rather than an inbound-data watchdog (the relay's 30s keepalive is a WebSocket control Ping, invisible to the channel stream, so a healthy idle subscription would be killed), this uses IOWebSocketChannel's protocol-level pingInterval: 30s, which closes the connection when pongs stop.

Out of scope, worth a follow-up: on relay CLOSED mobile drops the subscription from _liveSubscriptions with no retry path (relay_session.dart:496-523), and the desktop observer ingest silently discards frames on four branches without logging (observerRelayStore.ts:350-379).

Related issue

None found.

Testing

Both bugs were first reproduced with throwaway tests asserting the broken behaviour, then those tests were inverted into the regression tests in this PR:

  • agentWorkingSignal.test.mjs — a thread turn is visible to the conversation-scoped signal; channel scope unaffected.
  • conversationId.test.mjs — Rust-derived vectors, plus malformed channel/event IDs returning null.
  • relay_session_test.dart — short healthy resume keeps the socket; stale-socket resume reconnects and re-sends REQ.

Gates: just desktop-check, just desktop-test (3889 pass), just mobile-test (907 pass), just fix-all, flutter analyze.

Ruled out along the way, so the fix isn't aimed at the wrong layer: cold start with turn_started outside the 300s window, limit: 1000 truncation, terminal frames missing turnId, liveness pausing during permission prompts or long tool calls, and desktop socket staleness (desktop already has a 60s stall watchdog). Real liveness frames carry turnId + channelId at a 10s cadence and resurrect a pruned turn on their own — the failure was purely the UI scope.

No screenshots: the change is a conditional on an existing bar, and reproducing it needs a live agent working inside a thread.

Link to Devin session: https://app.devin.ai/sessions/0826f5720c94404fa8e327398aac6176
Requested by: @oscarlehuu

devin-ai-integration Bot and others added 3 commits August 1, 2026 05:42
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>
Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>
@oscarlehuu oscarlehuu self-assigned this Aug 1, 2026
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Oscar Le <oscar.lehuu@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Author

Desktop verification

Built with pnpm build:e2e and run in headed Playwright. Agent activity was injected only through __BUZZ_E2E_SEED_OBSERVER_EVENTS__; no typing events were emitted. Conversation UUIDs were independently computed in Python from crates/buzz-acp/src/conversation.rs and hardcoded in the assertions, so a TS/Rust mismatch would keep the bar dark.

03-thread-activity-visible

After the fix, Claude Opus: Working appears under both the channel and thread composers without typing activity.

before-no-thread-activity-bar

Before the fix, with ChannelPane.tsx temporarily reverted to typing-only, the channel bar appeared but the thread bar was empty under the same observer-only scenario.

02-other-thread-turn

A turn in another thread in the same channel lights only the channel bar; the open thread remains clean, confirming conversation scoping.

Also covered: root-based thread identity rather than nested reply IDs; opening an optimistic optimistic-<uuid> thread renders normally; deriveAgentConversationIdOrNull returns null without throwing; zero page or console errors.

Not covered: mobile end-to-end behavior (no emulator/relay run) or a real ACP agent; agent state came from the mock bridge.

devin-ai-integration Bot added a commit that referenced this pull request Aug 1, 2026
devin-ai-integration Bot added a commit that referenced this pull request Aug 1, 2026
@cursor
cursor Bot merged commit 68a952b into main Aug 1, 2026
5 checks passed
oscarlehuu added a commit that referenced this pull request Aug 7, 2026
Projects now leads with outcomes instead of plumbing. The landing is a
grid of outcome cards — needs-you / ready / in-flight / shipped-30d
counts, quiet projects dimmed, repository-less projects first-class —
sorted by what needs the user. A project opens as a page (breadcrumb
Projects › name) with a ship log of merged PRs, an in-flight section,
crew/upstream contributor split, and the old workspace tabs collapsed
into a Plumbing section, closed by default.

Clicking an in-flight thread opens the production MessageThreadPanel
in place on the project page (locked spike decision #13): the panel
owns its chrome (no nested shells), loads the root independently by
event id and replies through useThreadReplies — roots outside the
channel window render fine — with skeleton and error states, and the
composer send forwards threadHeadId so replies root correctly. The
channel send path is untouched (fallback only applies when a caller
supplies it). needsYou counts come from #74's store via a new
useNeedsYouForChannels hook with per-channel-set cached snapshots and
generation invalidation.

Three grok-4.5 review rounds: r1 caught a goChannel redirect and a
nonexistent PR.needsYou source; r2 caught the nested-panel shell,
channel-window-only reply loading, and count-based cache invalidation;
r3 verified every fix including no regression to channel thread sends.

Fixes #83

Signed-off-by: oscarlehuu <oscar.lehuu@gmail.com>
oscarlehuu added a commit that referenced this pull request Aug 7, 2026
Projects now leads with outcomes instead of plumbing. The landing is a
grid of outcome cards — needs-you / ready / in-flight / shipped-30d
counts, quiet projects dimmed, repository-less projects first-class —
sorted by what needs the user. A project opens as a page (breadcrumb
Projects › name) with a ship log of merged PRs, an in-flight section,
crew/upstream contributor split, and the old workspace tabs collapsed
into a Plumbing section, closed by default.

Clicking an in-flight thread opens the production MessageThreadPanel
in place on the project page (locked spike decision #13): the panel
owns its chrome (no nested shells), loads the root independently by
event id and replies through useThreadReplies — roots outside the
channel window render fine — with skeleton and error states, and the
composer send forwards threadHeadId so replies root correctly. The
channel send path is untouched (fallback only applies when a caller
supplies it). needsYou counts come from #74's store via a new
useNeedsYouForChannels hook with per-channel-set cached snapshots and
generation invalidation.

Three grok-4.5 review rounds: r1 caught a goChannel redirect and a
nonexistent PR.needsYou source; r2 caught the nested-panel shell,
channel-window-only reply loading, and count-based cache invalidation;
r3 verified every fix including no regression to channel thread sends.

Fixes #83

Signed-off-by: oscarlehuu <oscar.lehuu@gmail.com>
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