Skip to content

Commit 1cd98f9

Browse files
committed
fix(overlay): cross-session lock at user-scope with stale-lock recovery
The overlay queue lock was scoped to $TMPDIR, which on macOS is often a per-process folder under /var/folders/... - not shared across CC sessions. Two simultaneous CC sessions could each open their own tmux popup, breaking the serialization. Also: when a hook was SIGKILLed (OOM, timeout) the lock directory persisted forever, blocking every subsequent overlay until manually cleared. Fixes: - Lock lives at $(passthru_user_home)/passthru-overlay.lock.d, guaranteed shared across CC sessions of the same user. - New _OVERLAY_LOCK_STALE_AFTER threshold (default 180s, > overlay timeout + margin): if the existing lock's mtime is older than that, clear it and retry. Checked every ~2s during wait. - Default lock timeout raised from 90s to 180s to match typical user response time across multiple queued sessions.
1 parent b85ea65 commit 1cd98f9

1 file changed

Lines changed: 43 additions & 7 deletions

File tree

hooks/handlers/pre-tool-use.sh

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -801,12 +801,23 @@ fi
801801
export PASSTHRU_OVERLAY_UNALLOWED_SEGMENTS
802802

803803
# --- Overlay queue lock -------------------------------------------------------
804-
# CC can fire multiple PreToolUse hooks concurrently (parallel tool calls).
805-
# Only one overlay popup can be visible at a time in a given multiplexer.
806-
# Without serialization, the second+ hook falls through to CC's native dialog.
807-
# We use a mkdir-based lock to queue concurrent overlay invocations.
808-
_OVERLAY_LOCK="${_tmpdir}/passthru-overlay.lock.d"
809-
_OVERLAY_LOCK_TIMEOUT="${PASSTHRU_OVERLAY_LOCK_TIMEOUT:-90}"
804+
# CC can fire multiple PreToolUse hooks concurrently, and multiple CC sessions
805+
# on the same machine will also race for the same tmux/kitty/wezterm popup.
806+
# Only one overlay popup can be visible at a time. We serialize via a mkdir
807+
# lock at a user-scope path so the lock is shared across both intra-session
808+
# concurrent calls and cross-session concurrent calls.
809+
#
810+
# The lock MUST live under passthru_user_home, not TMPDIR, because macOS
811+
# gives each process a per-user (and sometimes per-session) TMPDIR under
812+
# /var/folders/... which is NOT shared across CC sessions.
813+
_OVERLAY_LOCK_ROOT="$(passthru_user_home)"
814+
[ -d "$_OVERLAY_LOCK_ROOT" ] || mkdir -p "$_OVERLAY_LOCK_ROOT" 2>/dev/null || true
815+
_OVERLAY_LOCK="${_OVERLAY_LOCK_ROOT}/passthru-overlay.lock.d"
816+
_OVERLAY_LOCK_TIMEOUT="${PASSTHRU_OVERLAY_LOCK_TIMEOUT:-180}"
817+
# If a lock is older than this, treat it as stale (process died without
818+
# releasing). Should be > PASSTHRU_OVERLAY_TIMEOUT (default 60s) plus
819+
# overlay launch + post-processing margin.
820+
_OVERLAY_LOCK_STALE_AFTER="${PASSTHRU_OVERLAY_LOCK_STALE_AFTER:-180}"
810821
_overlay_lock_acquired=0
811822

812823
_release_overlay_lock() {
@@ -816,8 +827,27 @@ _release_overlay_lock() {
816827
fi
817828
}
818829

819-
# Acquire the lock. Poll at 200ms intervals up to the timeout.
830+
# Detect and clear stale locks: if the lock directory exists but its mtime
831+
# is older than _OVERLAY_LOCK_STALE_AFTER seconds, a previous hook was
832+
# killed (SIGKILL, OOM, etc.) without running its EXIT trap. Force-clean.
833+
_clear_if_stale() {
834+
if [ -d "$_OVERLAY_LOCK" ]; then
835+
local mtime
836+
mtime="$(stat -f %m "$_OVERLAY_LOCK" 2>/dev/null || stat -c %Y "$_OVERLAY_LOCK" 2>/dev/null || echo 0)"
837+
local now age
838+
now="$(date +%s)"
839+
age=$((now - mtime))
840+
if [ "$age" -gt "$_OVERLAY_LOCK_STALE_AFTER" ]; then
841+
printf '[passthru] clearing stale overlay lock (age %ds)\n' "$age" >&2
842+
rm -rf "$_OVERLAY_LOCK" 2>/dev/null || true
843+
fi
844+
fi
845+
}
846+
847+
# Acquire the lock. Poll at 200ms intervals up to the timeout. Check for
848+
# stale locks every few iterations so a dead hook does not block forever.
820849
_lock_start="$(date +%s)"
850+
_stale_check_counter=0
821851
while true; do
822852
if mkdir "$_OVERLAY_LOCK" 2>/dev/null; then
823853
_overlay_lock_acquired=1
@@ -826,6 +856,12 @@ while true; do
826856
trap '_release_overlay_lock' EXIT
827857
break
828858
fi
859+
# Every 10th iteration (~2s), check for a stale lock.
860+
_stale_check_counter=$((_stale_check_counter + 1))
861+
if [ "$_stale_check_counter" -ge 10 ]; then
862+
_stale_check_counter=0
863+
_clear_if_stale
864+
fi
829865
_now="$(date +%s)"
830866
if [ $((_now - _lock_start)) -ge "$_OVERLAY_LOCK_TIMEOUT" ]; then
831867
printf '[passthru] overlay lock timeout after %ds; falling back to native dialog\n' "$_OVERLAY_LOCK_TIMEOUT" >&2

0 commit comments

Comments
 (0)