Skip to content

fix(tui): stop held peer message Allow/Deny from deadlocking Update - #1111

Merged
kevincodex1 merged 1 commit into
Twigpine:mainfrom
PierrunoYT:fix/tui-peer-decision-deadlock
Oct 14, 2026
Merged

kevincodex1 merged 1 commit into
Twigpine:mainfrom
PierrunoYT:fix/tui-peer-decision-deadlock

Conversation

@PierrunoYT

@PierrunoYT PierrunoYT commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Answering Allow or Deny on a held cross-session (peer) message hung the TUI. The prompt's decide closure called m.runtimeMessageSink(peerDecisionMsg{...}) from inside Update (via resolvePermissionWithReason). The sink ends in program.Send, which in bubbletea v2.0.9 does p.msgs <- msg on an unbuffered channel whose only reader is the event loop currently running Update, so it never returned (and Ctrl+C queued behind it).

This change adds a decideCmd field to pendingPermissionPrompt. The peer prompt uses it to return a tea.Cmd that yields peerDecisionMsg, and resolvePermissionWithReason returns that command instead of calling the sink. The existing peerDecisionMsg handling in Update is unchanged. Agent permission prompts keep using decide, which is called from the agent goroutine, not from Update.

Not included: the issue's optional "debug guard against sink calls on the Update goroutine".

Linked issue

Fixes #1099

Note: #1099 does not currently carry the issue-approved label. Opening this anyway at the author's request; it can be held until the issue is approved.

Verification

  • New regression test TestPeerApprovalDecisionDoesNotCallBlockingSinkFromUpdate installs a sink that blocks and resolves the held prompt with Allow and Deny. Without the fix it fails after the 5 s guard ("resolving a held peer prompt blocked on the runtime sink"); with the fix it passes.
  • TestPermissionMismatchHoldsPeerMessageForExplicitDecision and TestPeerApprovalDecisionWaitsForCompletionBeforeOpeningNext previously read the decision from a capturing sink, which is why they could not see the bug. They now take the decision from the returned command, and the first also asserts the sink is not called. They fail on the old code and pass on the new.
  • go build ./..., go vet ./internal/tui/, gofmt -l internal/tui, git diff HEAD --check clean; go test ./internal/tui/ passes.
  • Not run locally: go test -race (cgo unavailable here), go test ./..., smoke, make targets. Relying on CI for those.

Checklist

  • The linked issue already has the issue-approved label. (not yet)
  • go build ./..., go vet ./..., and go test ./... pass locally. (build passes; vet and tests only for ./internal/tui)
  • gofmt clean.
  • Tests added/updated for the change (and run under -race where relevant). (tests added; -race not run locally)
  • UI changes include screenshots or a short recording where possible. (no visual change)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Allowing or denying a peer permission request no longer waits for message delivery to complete, preventing the decision from stalling when delivery is blocked.
    • After a peer permission decision, the next queued approval can be presented.

Copilot AI balanced review requested due to automatic review settings October 2, 2026 17:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Asynchronous decisions can act after expiration or release, causing stale or duplicate delivery.

Review effort: Balanced
Findings: 1 High severity · 1 Low severity

Open (2)
What changed in this PR

Prevents held peer-message decisions from deadlocking the TUI event loop.

Changes:

  • Returns peer decisions through a Bubble Tea command.
  • Updates permission resolution to propagate decision commands.
  • Adds Allow/Deny regression tests.
File Description
internal/​tui/​peer_messages.go Emits peer decisions through commands.
internal/​tui/​model.go Supports command-based permission decisions.
internal/​tui/​peer_messages_test.go Tests nonblocking decision handling.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// runtimeMessageSink blocks on the event loop that is running Update.
decideCmd: func(decision agent.PermissionDecision) tea.Cmd {
decided := peerDecisionMsg{message: message, allow: decision.Action == agent.PermissionDecisionAllow}
return func() tea.Msg { return decided }
Comment thread internal/tui/model.go
Comment on lines +846 to +849
// decideCmd is the Update-safe alternative to decide for prompts the TUI
// itself raises: decide forwards through runtimeMessageSink, which blocks on
// the program's unbuffered message channel when called from Update, so these
// prompts return a command that yields the follow-up message instead.
@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: Twigpine/zero/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 353f6f9b-12c2-4229-9b19-e336a1365036
📥 Commits

Reviewing files that changed from the base of the PR and between a72684b and 255a27c.

📒 Files selected for processing: 1
  • internal/tui/model.go

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.

Walkthrough

Peer permission decisions now return Bubble Tea commands instead of being sent synchronously through the runtime message sink. Permission resolution returns or batches the command. Tests check the decision, message ID, and behavior with a blocked sink.

Changes

Peer permission resolution

Layer / File(s) Summary
Return and verify peer decision commands
internal/tui/model.go, internal/tui/peer_messages.go, internal/tui/peer_messages_test.go
Permission resolution captures and returns the decision command. The peer callback creates a command that emits the decision message. Tests inspect allow and deny commands, verify resolution completes with a blocked sink, and retain queued-approval checks.

Priority: ➖ Normal

Estimated code review effort: 2 (Simple) | ~12 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: anandh8x, vasanthdev2004, gnanam1990

Merge Risk: ⚪ Minimal · up to 255a2

Peer approvals now avoid the synchronous runtime-sink call associated with the TUI deadlock, while queued approvals retain their existing progression. No material merge-blocking risk is identified; the change appears ready for normal checks.

### Architecture Summary

Architecture risk: 🔵 Low · up to 255a2

The change affects 1 system.

Changed systems: internal

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — internal (service) was modified; 3 changed files map to changed impact.

Before / after behavior

  • observed — Modified behavior in internal/tui/peer_messages.go: The approval callback now returns a command that emits the peer decision message; it no longer synchronously invokes runtimeMessageSink.
  • observed — Modified behavior in internal/tui/peer_messages_test.go: The test file adds the time import for the blocking-sink timeout assertion.
  • observed — Modified behavior in internal/tui/peer_messages_test.go: The approval test now verifies that allowing a held peer message returns an allow decision command for held-1, and that no message is sent through the runtime sink during resolution. Previously it expected the decision to be enqueued on that sink.
  • observed — Modified behavior in internal/tui/peer_messages_test.go: Adds peerDecisionFromCmd, which executes a command and recursively searches nested tea.BatchMsg commands for the first peerDecisionMsg; it returns false for a nil command or when none is found.
Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: preventing the TUI deadlock caused by Allow/Deny decisions for held peer messages.
Linked Issues check Passed Direct issue [#1099] requires Allow and Deny for a held peer message to avoid the blocking runtime sink during Update, with regression coverage. openNextPeerApproval now returns a tea.Cmd that e…
Out of Scope Changes check Passed The changes stay within [#1099]. Production changes affect peer permission resolution and command delivery. Test changes verify the deadlock fix and peer-approval ordering. No unrelated behavior or un…
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.

Comment @coderabbitai help to get the list of available commands.

@kevincodex1

Copy link
Copy Markdown
Member

hello @PierrunoYT please check the failing CI test

The peer approval prompt's decide closure called runtimeMessageSink from
inside Update. The sink ends in program.Send, which blocks on bubbletea's
unbuffered message channel; the only reader is the event loop that is
running Update, so answering a held cross-session message hung the TUI.

Give pendingPermissionPrompt a decideCmd that returns a tea.Cmd yielding
the peerDecisionMsg, and have resolvePermissionWithReason return it
instead of calling the sink. Tests now drive the decision through the
returned command and a blocking sink, which hangs on the old code.

Fixes Twigpine#1099

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@PierrunoYT
PierrunoYT force-pushed the fix/tui-peer-decision-deadlock branch from a72684b to 255a27c Compare October 14, 2026 11:32
@PierrunoYT

Copy link
Copy Markdown
Contributor Author

@kevincodex1 The failing check was Smoke (windows-latest) in internal/tools: TestExecCommandForegroundServerReturnsSessionAndServesHTTP hit its 500 ms wait before the test HTTP server printed its address. This PR only touches internal/tui, that test hasn't changed on main, and it passes 10/10 locally on Windows, so it looks like a timing flake on the runner.

I rebased the branch onto current main (255a27c). The re-run is fully green, including Windows smoke and the race detector. If the test flakes again, I'll open a separate issue to make it poll for the address instead of waiting once.

@kevincodex1
kevincodex1 merged commit a195e88 into Twigpine:main Oct 14, 2026
10 checks passed
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.

TUI deadlocks when answering a held peer message (Allow/Deny)

3 participants