Skip to content

fix: prevent duplicate control response for already-handled requests (#229)#230

Merged
Nathan Schram (nathanschram) merged 2 commits intodevfrom
fix/control-request-expiry-duplicate-229
Mar 27, 2026
Merged

fix: prevent duplicate control response for already-handled requests (#229)#230
Nathan Schram (nathanschram) merged 2 commits intodevfrom
fix/control-request-expiry-duplicate-229

Conversation

@nathanschram
Copy link
Copy Markdown
Member

Summary

  • Fix control request expiry sending duplicate DENY for requests already approved via Telegram callback
  • Add reconciliation loop in translate() that syncs _HANDLED_REQUESTS with state.pending_control_requests
  • Emit action_completed to clear stale inline keyboards from progress messages

Fixes #229

What happened

When a user clicks Approve on a control request (e.g. ExitPlanMode), send_claude_control_response() writes the response to Claude Code's stdin and marks it in _HANDLED_REQUESTS. But it can't access state.pending_control_requests (local to the translate loop). After 5 minutes, the expiry check finds the already-handled request and sends a second DENY — Claude Code receives conflicting approve+deny responses for the same request_id and stalls.

The upstream Claude Code freeze (ignoring a valid approval written to stdin) is tracked in anthropics/claude-code#39666.

Changes

src/untether/runners/claude.py:

  • Add request_to_action mapping on ClaudeStreamState (request_id → action_id)
  • Reconciliation loop before expiry check: removes handled requests from pending, emits action_completed
  • Guard on expiry list comprehension: rid not in _HANDLED_REQUESTS

tests/test_claude_control.py:

  • test_handled_request_not_auto_denied_on_expiry — verifies handled requests skip auto-deny
  • test_reconciliation_emits_action_completed_for_stale_keyboard — verifies keyboard cleanup

Test plan

  • All 1817 tests pass (81.39% coverage)
  • Lint clean (ruff check)
  • Dev service starts cleanly
  • Integration test: send task in plan mode via @untether_dev_bot, approve ExitPlanMode, verify no stale keyboard and no expired_auto_deny in logs

🤖 Generated with Claude Code

Update error hint text from "corrupted during a restart" to "archived or
expired" — better reflects the actual cause when Claude Code auto-archives
a session between resume runs.

Related: anthropics/claude-code#39178

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…229)

When a user clicks Approve/Deny on a control request via Telegram,
send_claude_control_response() marks it in _HANDLED_REQUESTS but can't
access state.pending_control_requests. The 5-minute expiry check then
sends a duplicate DENY for the same request_id, causing Claude Code to
receive conflicting approve+deny responses and stall.

Add reconciliation in translate() that checks _HANDLED_REQUESTS against
pending_control_requests before the expiry loop:
- Removes already-handled requests from pending (prevents spurious deny)
- Emits action_completed to clear stale inline keyboards
- Adds belt-and-suspenders guard on the expiry list comprehension

The upstream Claude Code freeze is tracked in anthropics/claude-code#39666.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a8a8c877-f232-45f7-b7c3-26de48d8611c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/control-request-expiry-duplicate-229

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@nathanschram Nathan Schram (nathanschram) merged commit 756182c into dev Mar 27, 2026
20 of 21 checks passed
@nathanschram Nathan Schram (nathanschram) deleted the fix/control-request-expiry-duplicate-229 branch March 27, 2026 02:48
@nathanschram Nathan Schram (nathanschram) added this to the v0.35.0 milestone Mar 29, 2026
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