Skip to content

in_approval flag has no timeout — stuck flag freezes all approvals #41

@martymcenroe

Description

@martymcenroe

Summary

The in_approval flag is a boolean that gates whether unleashed will attempt to auto-approve a detected permission prompt. It's set to True at the start of do_approval() and cleared to False in _sentinel_check()'s finally block (or immediately for non-sentinel approvals).

The problem: there is no watchdog, no timeout, and no recovery mechanism if the flag gets stuck at True.

# In _sentinel_check():
def _sentinel_check(self, pty, tool_type, tool_args):
    try:
        # ... API call, verdict handling ...
    except Exception as e:
        # ... fail-open ...
    finally:
        self.in_approval = False  # <-- only reset point for sentinel path

If anything prevents finally from executing — thread killed externally, Python interpreter issue, pty.write() hanging in the try block — in_approval stays True and every subsequent permission prompt is ignored. The user sees Claude Code asking for permission, but unleashed never sends the CR keystroke. The session appears frozen.

Reproduction Scenario

  1. Enable sentinel (--sentinel or --sentinel-scope)
  2. Trigger a permission prompt for a command that routes through sentinel
  3. Simulate _sentinel_check hanging: e.g., Haiku API returns but pty.write('\r') blocks because the PTY buffer is full
  4. finally never executes
  5. Next permission prompt: in_approval is still True, so _reader_pty() skips the do_approval() call
  6. Session is stuck — user must Ctrl+C and restart

Why This Is Priority High

Even without sentinel, the non-sentinel do_approval() path sets in_approval = True before the time.sleep(0.1) + pty.write('\r') sequence. If pty.write raises (PTY died), in_approval stays True. The flag is never checked against time — there's no "if it's been True for >10 seconds, something is wrong."

This is a reliability issue first and a sentinel issue second. The flag predates sentinel integration.

Proposed Fix

Add a watchdog in _reader_pty():

# In the permission detection block:
if self.in_approval:
    # Watchdog: if in_approval has been True for >15s, force-reset
    if hasattr(self, '_approval_started') and time.time() - self._approval_started > 15:
        log("WATCHDOG: in_approval stuck for >15s, force-resetting")
        sys.stderr.write("\n\033[93m[UNLEASHED] Approval timeout — resetting\033[0m\n")
        self.in_approval = False
    continue

And timestamp when approval starts:

def do_approval(self, pty, tool_type="unknown", tool_args=""):
    self.in_approval = True
    self._approval_started = time.time()
    # ...

15 seconds is generous — even a slow Haiku call with retry would complete in <10s. If we're still in approval after 15s, something is broken.

Affected Files

  • src/unleashed-c-21.pydo_approval(), _sentinel_check(), _reader_pty() approval detection block

Context

The in_approval flag is the primary serialization mechanism for unleashed's auto-approval system. It prevents double-approval (sending multiple CRs for one prompt) and prevents the PTY reader from processing output that arrives between the permission detection and the CR send. But a flag without a timeout is a latch without a spring — if it gets stuck, the whole mechanism stops. Every long-running unleashed session is one edge case away from this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingon-iceFeature/fix on hold — not in active developmentpriority:highFix before next production promotion

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions