Fix Remote speech stalls after pause and early completion

This commit is contained in:
ak47 committed 2026-11-06 22:11:29 -05:00
1 parent 6867d901b2
commit 19b789ef5c
6 files changed
+131 -30

No files matched your search

+26 -13
View File
@@ -38,6 +38,29 @@ func (client *Client) speak_and_wait_locked(ctx context.Context, speak func() er
}
}
}()
// Notifications can precede the SPEAK queue acknowledgement. Handle them
// while reading command replies too, or an early END/CANCEL is lost and
// this owner waits forever while later remote utterances queue behind it.
started, completed := false, false
var completion error
handle := func(event reply) {
if completed {
return
}
switch event.code {
case 701:
if !started && ctx.Err() == nil && began != nil {
began()
}
started = true
case 702:
completed, completion = true, ctx.Err()
case 703:
completed, completion = true, context.Canceled
}
}
client.notification = handle
defer func() { client.notification = nil }()
if err := speak(); err != nil {
return err
}
@@ -61,24 +84,14 @@ func (client *Client) speak_and_wait_locked(ctx context.Context, speak func() er
<-done
}
}()
started := false
for {
for !completed {
event, err := readReply(client.reader)
if err != nil {
_ = conn.Close()
client.conn, client.reader = nil, nil
return errors.Join(ctx.Err(), err)
}
switch event.code {
case 701:
if !started && ctx.Err() == nil && began != nil {
began()
}
started = true
case 702:
return ctx.Err()
case 703:
return context.Canceled
}
handle(event)
}
return errors.Join(ctx.Err(), completion)
}
+39
View File
@@ -82,3 +82,42 @@ func TestContinuousSpeechWaitsForEndAndSurvivesCancellation(t *testing.T) {
})
}
}
func TestContinuousSpeechCompletionBeforeQueueReply(t *testing.T) {
for _, terminal := range []int{702, 703} {
t.Run(fmt.Sprint(terminal), func(t *testing.T) {
conn, server := net.Pipe()
client := connectedClient(conn)
t.Cleanup(func() { server.Close(); client.Close(context.Background()) })
began := make(chan struct{}, 1)
finished := make(chan error, 1)
go func() {
finished <- client.SpeakAndWait(t.Context(), "Short remote response", func() { began <- struct{}{} })
}()
server.SetDeadline(time.Now().Add(2 * time.Second))
reader := bufio.NewReader(server)
if err := exchangeSSIP(reader, server, "SET SELF NOTIFICATION ALL on\r\n", "200 OK\r\n"); err != nil {
t.Fatal(err)
}
for range 3 {
if _, err := reader.ReadString('\n'); err != nil {
t.Fatal(err)
}
}
// Speech Dispatcher queues work before returning the SPEAK reply.
// A short or preempted utterance can finish before that reply arrives.
fmt.Fprintf(server, "230 OK\r\n701-1\r\n701-42\r\n701 BEGIN\r\n%d-1\r\n%d-42\r\n%d DONE\r\n225-1\r\n225 QUEUED\r\n", terminal, terminal, terminal)
if err := exchangeSSIP(reader, server, "SET SELF NOTIFICATION ALL off\r\n", "200 OK\r\n"); err != nil {
t.Fatal("completion before queue reply was lost:", err)
}
if err := <-finished; terminal == 703 && !errors.Is(err, context.Canceled) || terminal == 702 && err != nil {
t.Fatalf("completion = %v", err)
}
select {
case <-began:
default:
t.Fatal("early BEGIN was lost")
}
})
}
}
+4
View File
@@ -47,6 +47,7 @@ type Client struct {
mutex sync.Mutex
cancelMutex sync.Mutex
settingsMutex sync.Mutex
notification func(reply) // Speaking mutex owns the active completion handler.
}
type Settings struct {
@@ -426,6 +427,9 @@ func (client *Client) readReply() (reply, error) {
if err != nil || response.code < 700 || response.code >= 800 {
return response, err
}
if client.notification != nil {
client.notification(response)
}
}
}