Skip to content

fix(peer): route Tauri events once - #2384

Merged
bobleer merged 1 commit into
GCWing:mainfrom
bobleer:bob/fix-tauri-event-fanout
Aug 19, 2026
Merged

fix(peer): route Tauri events once#2384
bobleer merged 1 commit into
GCWing:mainfrom
bobleer:bob/fix-tauri-event-fanout

Conversation

@bobleer

@bobleer bobleer commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes the busy-hang where a conversation never settles after a turn completes, so every later message is stuck in the pending queue and only a new conversation recovers.

14c4b5c73 (#2325) moved the stateful runtime-session cursor routing into every logical Tauri listener. SessionStream is a per-(surface, session) ordering gate whose cursor dedup exists to suppress a re-delivered event (snapshot/live overlap), so it must decide about a delivery. With one native listener per logical subscriber, a single delivery was pushed through that gate once per subscriber: the first advanced the applied cursor, and every later subscriber was refused as not-ahead and dropped.

agentic://dialog-turn-completed has several independent subscribers (turn settlement in AgenticEventListener, useAssistantBootstrap, useDialogCompletionNotify, useAcpPlan), so which one received it depended on registration order. Whenever the settlement subscriber was not the winner, the frontend state machine stayed in PROCESSING forever. agentic://dialog-turn-started and agentic://dialog-turn-interrupted have a single subscriber each, which is why only the terminal event was affected.

This cherry-picks the fix from 1.0.0-explore (5bb566c14, authored by @wsp) onto main unchanged, so the two branches do not diverge. Each native Tauri listener is shared across all adapter instances, surface and cursor routing run once per delivery, and logical subscribers are then fanned out. Subscription ownership stays isolated per adapter and held-event delivery semantics are preserved — that also closes the cross-adapter variant that Peer Device Mode can hit while several transport adapters are alive.

Closes #2381

Runtime evidence

Diagnosed on macOS Desktop with instrumentation at the emit site, the host delivery site, the surface gate, the SessionStream admission point and the settlement handler.

Before, for every completed turn: the backend emitted once (emit_lifecycle_events: true, success: true) and the host delivered once, but the webview surface gate saw the event 3 times and SessionStream returned 1 apply + 2 drop. On the reproducing run the settlement subscriber was a drop, so handleDialogTurnComplete never ran and the machine stayed in PROCESSING while the backend was already Idle with the turn persisted.

After, across 13 consecutive turns in 5 sessions: 1 arrival, 1 apply, 0 drop per completed turn, settlement reached finalizeTurnCompletionState 13/13, and 3 messages queued while busy all drained.

Note for the issue reporter: the report attributed the regression to #2372 lock contention. Instrumentation rejected that — the completion path held the mutation lock for ~4 ms, the backend reset to Idle correctly, and the second message was never submitted to the backend at all. The regression source is #2325.

Test plan

  • pnpm --dir src/web-ui run test:run src/infrastructure/api/adapters src/infrastructure/peer-device src/flow_chat/session-stream — 14 files, 137 tests pass
  • pnpm run type-check:web — no errors in changed files (3 pre-existing @/generated/api errors come from the gitignored codegen artifact, .gitignore:51)
  • Desktop, local workspace: 13 turns across 5 sessions settle, including turns with 13/20/28/31/40 tool rounds, session switches mid-turn, and messages queued while busy
  • Peer Device Mode cross-adapter fan-out — covered by the added regression tests, not exercised on hardware

The regression was introduced by Bob Lee in commit
14c4b5c, which moved the stateful
runtime-session cursor routing into every logical Tauri listener. The first
subscriber advanced SessionStream, causing later subscribers to drop the same
terminal event.

Share each native Tauri listener across all adapter instances so surface and
cursor routing runs once before logical subscriber fan-out. Keep subscription
ownership isolated per adapter and preserve held-event delivery semantics.

Add regressions for cross-adapter fan-out, adapter-specific disconnect, and
attachment-held events.

Refs: 14c4b5c
@bobleer
bobleer merged commit 0825520 into GCWing:main Aug 19, 2026
8 checks passed
@bobleer
bobleer deleted the bob/fix-tauri-event-fanout branch August 19, 2026 18:42
1688mengdie pushed a commit to 1688mengdie/BitFun that referenced this pull request Aug 19, 2026
Sync upstream 0825520 (PR GCWing#2384 fix(peer): route Tauri events once) - tauri-adapter.ts + test, zero conflicts
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.

[Bug]: Conversation stuck busy-hang on the second message (regression from #2372)

2 participants