fix: bound system TTS Speak with a process deadline - #10
Conversation
systemTTSEngine.Speak ran espeak-ng via exec.Command with no deadline. A hung TTS child blocked the single TTS queue forever. Use CommandContext with a 30s timeout so a hung Speak returns instead of parking the queue. Signed-off-by: Sebastien Tardif <[email protected]>
|
🦞👀 Pull request received. I will update this pull request when review starts. |
|
Codex review: blocked before merge. Reviewed September 3, 2026, 11:03 AM ET / 15:03 UTC. ClawSweeper reviewWhat this changesThe PR runs each system TTS command under a 30-second process deadline and adds a regression test for a hung child process. Merge readiness⛔ Blocked before merge - 4 items remain Keep this PR open: the deadline prevents a hung child from blocking the TTS queue, but its unconditional 30-second cutoff now terminates valid long chat responses and slower configured system-TTS commands. Priority: P1 Review scores
Verification
How this fits togetherClawgo receives final chat text from the gateway and sends it through a single local TTS queue. The queue invokes the configured system speech command, whose completion determines when the next spoken response can start. flowchart LR
A[Gateway chat final] --> B[Chat subscriber]
B --> C[Single TTS queue]
C --> D[System TTS engine]
D --> E[Deadline decision]
E --> F[Local speech command]
F --> G[Spoken response or error log]
Decision needed
Why: The PR changes established speech behavior for every existing system-TTS user, and choosing a fixed, configurable, or length-aware budget is a compatibility contract rather than a mechanical correction. Before merge
Findings
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Keep the hang guard, but derive a bounded speech budget from text length and configured rate with a conservative ceiling, then prove both a hung child and a healthy utterance exceeding 30 seconds behave as intended. Do we have a high-confidence way to reproduce the issue? Yes. The supplied terminal trace exercises the production Speak path with a real sleeping child, and source inspection shows current main remains unbounded; this review did not execute the trace locally. Is this the best way to solve the issue? No. CommandContext is an appropriate hang mechanism, but a fixed 30-second budget is not the narrowest compatible solution because final chat text and custom system commands have no corresponding duration bound. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: not found in the target repository. Codex review notes: model internal, reasoning high; reviewed against c6e46796a1c8. LabelsLabel justifications:
EvidenceWhat I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (1 earlier review cycle)
|
What Problem This Solves
Default
clawgo run(-chat-subscribe true,-tts-engine system) speaks chatfinaltext through a single TTS queue. That queue callssystemTTSEngine.Speak, which ranespeak-ng(or-tts-system-command) withexec.CommandandRun(). There was no context and no process deadline.If the TTS binary hangs, the queue goroutine stays inside
Run()forever. Later chat speech sits in the buffer-16 channel, then is dropped when the channel is full. One stuckespeak-ng(or a stand-in such assleep) silences the node untilclawgois killed.This is separate from #7, which stops a leaked queue on reconnect. Here the current queue is alive and blocked on one child.
Evidence
Live
go runof the oldexec.Commandpath versusCommandContextwith a 200ms deadline. Child issleep 2, the same argv shape Speak uses (commandplus the spoken text as the last argument):Same two items on a serial queue (the TTS loop shape). Without a deadline, item 2 cannot start until
sleep 2finishes. With a 200ms deadline, item 1 is killed and item 2 runs:On this branch,
systemTTSEngine.Speakuses thatCommandContextpath. A hung child (sleep 2, 200ms deadline) now returns an error in 0.20s instead of succeeding after 2s.Real behavior proof
fix/tts-speak-deadlineat/tmp/clawgo-F005.go run /tmp/clawgo-f005-speak-demo.goandgo run /tmp/clawgo-f005-queue-demo.go. Then invoked productionsystemTTSEngine.Speakwith commandsleep, text2, and a 200ms deadline.go runhelpers above. Unboundedsleep 2returned nil after 2.008s. Bounded Speak killed the child at 201ms (signal: killed). The serial queue then started the next item at 203ms total instead of waiting the full 2s.exec.Commandpath still waits for the child to exit on its own.espeak-nghang, utterances longer than 30s, and grandchild processes that outlive the killed TTS parent.Summary
Call chain: chat
final->ChatSubscriber.speak->TTSQueue.Speak->TTSQueue.loop->systemTTSEngine.Speak->exec.Command(...).Run().Fix:
exec.CommandContextwith a 30s deadline (defaultTTSSpeakTimeout). The queue already logstts error: %vwhen Speak fails.Introduced in
f601408(2026-01-04, 241 days). Still present after thec6e4679rewrite in #8.Related work:
modules/stt/brabble.goalready starts the STT child withexec.CommandContext.exec.CommandContextkills the process when the context is done.