-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Summary
Every time sentinel needs to evaluate a command, do_approval() spawns a new daemon thread:
# In do_approval():
t = threading.Thread(
target=self._sentinel_check,
args=(pty, tool_type, tool_args),
daemon=True
)
t.start()
return # PTY reader resumes immediatelyThere is no thread pool, no concurrency limit, and no tracking of active sentinel threads. While the in_approval flag prevents sequential rapid-fire (the next permission won't trigger while the current one is being evaluated), there are edge cases:
-
Sentinel timeout (3s) + rapid Claude output: If the Haiku API is slow and Claude somehow emits another permission prompt before
in_approvalclears (e.g., due to a race between the worker thread'sfinallyblock and the PTY reader's pattern match), a new thread spawns. -
Thread leak on exception: If
_sentinel_checkraises before reachingfinally(theoretically impossible given the try/except/finally structure, but defensive coding matters for a security component),in_approvalstays True forever and the thread is leaked. -
No visibility: There's no way to know how many sentinel threads are alive during a session. No logging of thread count, no max-threads guard.
Impact
- Normal operation: Minimal —
in_approvalflag serializes evaluations effectively - Degraded API: If Haiku is slow (>3s consistently), threads accumulate waiting for timeout
- Worst case: Thread exhaustion, though Python's threading limit is high (~thousands)
Proposed Fix
Replace ad-hoc thread creation with a single-slot worker:
# In __init__:
self._sentinel_executor = concurrent.futures.ThreadPoolExecutor(
max_workers=1,
thread_name_prefix="sentinel"
)
# In do_approval():
self._sentinel_executor.submit(self._sentinel_check, pty, tool_type, tool_args)Single worker ensures:
- At most one sentinel check in flight at any time
- Subsequent submissions queue (won't spawn extra threads)
- Clean shutdown via
executor.shutdown()in cleanup - Thread naming for debugging (
sentinel_0)
Affected Files
src/unleashed-c-21.py—do_approval()and_sentinel_check()
Context
The worker thread pattern was chosen specifically to avoid the deadlock that killed the previous integration attempt (archive/unleashed-guarded.py). It works correctly for the common case. This issue is about hardening the uncommon case — making the thread lifecycle deterministic and bounded. A ThreadPoolExecutor(max_workers=1) preserves all the benefits of the current design while adding lifecycle management for free.