From e3e9a08b53a0b13046394fd5884b8c4cf8d1387a Mon Sep 17 00:00:00 2001 From: TheDiscordian Date: Thu, 4 Jun 2026 00:35:31 -0400 Subject: [PATCH] Fix TTS streamer hang that freezes crab stop indefinitely The TTS streamer tails the stream-json debug log and only exits when it reads a {"type":"result"} event. Two paths left it looping forever, with the parent crab stop blocked on `wait` for it: 1. If claude crashed, was killed, or got rate-limited mid-stream it may never emit a result event. 2. A streamer still running from an overlapping request gets its read cursor stranded past EOF when the next session truncates the shared log, so it never reaches the result event. Fixes: - Append a synthetic {"type":"result"} terminator after the claude call so the streamer always gets a stop signal (extract-response ignores a result line with no "result" field, so it's harmless on success). - Kill any prior streamer before truncating the shared log, so an overlapping streamer can't be stranded. - Exit the streamer if its launching crab process dies (ppid changes), covering the orphaned-parent case under a systemd user subreaper. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/common.sh | 13 +++++++++++++ lib/tts-streamer | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/lib/common.sh b/lib/common.sh index 19e22eb..b5d6b25 100644 --- a/lib/common.sh +++ b/lib/common.sh @@ -171,6 +171,11 @@ PROMPT # Start background TTS streamer that reads from DEBUGLOG start_tts_streamer() { + # Kill any prior streamer before truncating the shared log. A streamer left + # running from an overlapping request keeps its read cursor at the old + # offset; truncating the log strands that cursor past EOF and the streamer + # then tails the file forever — hanging the crab stop that waits on it. + pkill -f "$LIB_DIR/tts-streamer" 2>/dev/null : > "$DEBUGLOG" DESKCRAB_DEBUGLOG="$DEBUGLOG" DESKCRAB_PIPER_VOICE="$PIPER_VOICE" \ DESKCRAB_PIPER_LENGTH_SCALE="${PIPER_LENGTH_SCALE:-}" \ @@ -203,6 +208,14 @@ run_claude_and_respond() { --append-system-prompt "$SYSTEM_PROMPT" \ "$TEXT" > "$DEBUGLOG" 2>&1 + # Guarantee the TTS streamer always receives a stop signal. claude normally + # ends its stream with a {"type":"result"} line, but if it crashed, was + # killed, or got rate-limited mid-stream it may not — and without a result + # event the streamer tails the log forever and the wait below never returns. + # extract-response ignores a result line that has no "result" field, so this + # terminator is harmless on the success path. + printf '{"type":"result"}\n' >> "$DEBUGLOG" + # Dismiss thinking notification notify-send -t 1 -h string:x-dunst-stack-tag:deskcrab "$NOTIFY_NAME" "" 2>/dev/null diff --git a/lib/tts-streamer b/lib/tts-streamer index 4aaa08c..7b8cf7b 100755 --- a/lib/tts-streamer +++ b/lib/tts-streamer @@ -39,6 +39,12 @@ def speak(text): pass +# Remember who launched us (the crab process). If it dies we get reparented — +# to PID 1 or, under a systemd user manager, to a subreaper — either way our +# ppid changes. That's our cue to stop, since an orphaned crab can no longer +# append the terminator and we must not tail the log forever. +ORIG_PPID = os.getppid() + while not os.path.exists(LOG): time.sleep(0.1) @@ -46,6 +52,11 @@ with open(LOG) as f: while True: line = f.readline() if not line: + # Backstop: if our launching crab process is gone we've been + # reparented (ppid changes) — it can no longer append a terminator, + # so stop instead of tailing the log forever. + if os.getppid() != ORIG_PPID: + break time.sleep(0.05) continue line = line.strip()