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()