Add bounded incoming Remote speech stream ownership

This commit is contained in:
ak47 committed 2026-11-04 20:10:47 -05:00
1 parent ac431dd514
commit 914d174925
3 files changed
+733 -9

No files matched your search

+218
View File
@@ -0,0 +1,218 @@
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"encoding/json"
"errors"
"time"
"git.stormux.org/storm/cthulhu/internal/tentacle"
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
)
type remoteSpeechStreamItem struct {
delivery tentacle.RemoteSpeechDelivery
utterance remoteSpeechUtterance
bytes int
}
// run_remote_speech_playback supplies the isolated backend to the stream owner.
// It is deliberately not registered with production sessions yet: the caller
// still needs the reader's local-priority and delivery-time policy integration.
func run_remote_speech_playback(ctx context.Context, speaker *configuredSpeaker,
incoming <-chan tentacle.RemoteSpeechDelivery, allow func(tentacle.RemoteSpeechDelivery) bool) error {
if speaker == nil || allow == nil {
return errors.New("Remote speech playback requires a backend and delivery policy")
}
var voice *remoteSpeechVoice // Only the renderer accesses it until run joins.
var voiceLease tentaclewire.RemoteLease
var stopLease func() bool
closeVoice := func() {
if stopLease != nil {
stopLease()
}
if voice != nil {
cleanup, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_ = voice.Close(cleanup)
}
}
defer closeVoice()
return run_remote_speech_stream(ctx, incoming, allow, func(request context.Context, delivery tentacle.RemoteSpeechDelivery, utterance remoteSpeechUtterance) error {
if voice == nil || voice.ctx.Err() != nil || voiceLease != delivery.Lease {
closeVoice()
var err error
voice, err = speaker.open_remote_voice(request)
if err != nil {
return err
}
voiceLease = delivery.Lease
owned := voice
// Even an idle handle belongs to its original lease. Close it on
// revoke, on a backend worker rather than the policy/input caller.
stopLease = context.AfterFunc(delivery.Context, func() {
cleanup, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_ = owned.Close(cleanup)
})
}
// Connecting/querying the backend can take time. Admission cannot
// substitute for the original authority at synthesis handoff.
if request.Err() != nil {
return request.Err()
}
if !allow(delivery) {
return errors.New("Remote speech delivery policy changed")
}
return voice.SpeakUtteranceAndWait(request, utterance)
})
}
// run_remote_speech_stream is an unwired reader-owner prerequisite. allow must
// recheck original lease, focus and output policy without backend I/O. render
// must honor cancellation, recheck policy after acquisition, and complete scoped
// backend cleanup before returning. Neither callback may observe this output.
// The caller must retire the session on error. A dequeue retains mailbox budget
// until completion/discard, including while a cancelled renderer is closing.
func run_remote_speech_stream(ctx context.Context, incoming <-chan tentacle.RemoteSpeechDelivery,
allow func(tentacle.RemoteSpeechDelivery) bool,
render func(context.Context, tentacle.RemoteSpeechDelivery, remoteSpeechUtterance) error) error {
if allow == nil || render == nil {
return errors.New("Remote speech owner requires delivery policy and renderer")
}
var pending []*remoteSpeechStreamItem
var active *remoteSpeechStreamItem
var finished chan error
var cancel context.CancelFunc
var activeContext context.Context
var lease tentaclewire.RemoteLease
var leaseDone <-chan struct{}
paused := false
items, bytes := 0, 0
valid := func(delivery tentacle.RemoteSpeechDelivery) bool {
return ctx.Err() == nil && delivery.Context != nil && delivery.Context.Err() == nil && allow(delivery)
}
release := func(item *remoteSpeechStreamItem) {
items -= len(item.delivery.Speeches)
bytes -= item.bytes
item.delivery.Release()
}
discard := func() {
for _, item := range pending {
release(item)
}
pending = nil
}
interrupt := func() {
discard()
if cancel != nil {
cancel()
}
}
defer func() {
interrupt()
if active != nil {
<-finished // Renderer owns cleanup; no replacement can overtake it.
release(active)
}
}()
for {
if ctx.Err() != nil {
return ctx.Err()
}
for active == nil && len(pending) != 0 {
item := pending[0]
pending[0] = nil
pending = pending[1:]
if !valid(item.delivery) {
release(item)
continue
}
request, stop := context.WithCancel(item.delivery.Context)
cancel, active, activeContext, finished = stop, item, request, make(chan error, 1)
go func(result chan<- error) { result <- render(request, item.delivery, item.utterance) }(finished)
}
select {
case <-ctx.Done():
return ctx.Err()
case <-leaseDone:
interrupt()
leaseDone = nil
case err := <-finished:
// A deliberate stream interruption is not a backend failure. Never
// retry its suffix; the renderer may have spoken a prefix already.
interrupted := activeContext.Err() != nil
cancel()
release(active)
active, cancel, finished = nil, nil, nil
if err != nil && !interrupted {
return errors.New("Remote speech renderer failed")
}
case delivery, ok := <-incoming:
if !ok {
return nil
}
if delivery.Release == nil {
return errors.New("Remote speech delivery has no budget owner")
}
if !valid(delivery) {
delivery.Release()
continue
}
if lease != delivery.Lease {
interrupt()
lease, leaseDone, paused = delivery.Lease, delivery.Context.Done(), false
}
if len(delivery.Speeches) == 1 {
effect := delivery.Speeches[0]
if tentaclewire.ValidRemoteSpeech(effect) && (effect.Kind == "cancel" || effect.Kind == "pause") {
if effect.Kind == "cancel" || *effect.Paused {
interrupt()
}
if effect.Kind == "pause" {
paused = *effect.Paused
}
delivery.Release()
continue
}
}
utterance, err := prepare_remote_utterance(delivery.Speeches)
if err != nil {
delivery.Release()
return err
}
if paused {
delivery.Release()
continue
}
item := &remoteSpeechStreamItem{delivery: delivery, utterance: utterance}
for _, frame := range delivery.Speeches {
encoded, _ := json.Marshal(frame)
item.bytes += len(encoded)
}
if items+len(delivery.Speeches) > tentaclewire.RemoteSpeechQueueSize || bytes+item.bytes > tentaclewire.RemoteSpeechQueueBytes {
delivery.Release()
return errors.New("Remote speech owner budget exceeded")
}
items += len(delivery.Speeches)
bytes += item.bytes
// NEXT waits for the current assembly. NOW interrupts only lower
// priority work; equal priorities remain FIFO. Interrupted speech
// is discarded, because this backend cannot resume without replay.
if utterance.priority == 2 && active != nil && active.utterance.priority < 2 {
cancel()
}
position := len(pending)
for position > 0 && pending[position-1].utterance.priority < utterance.priority {
position--
}
pending = append(pending, nil)
copy(pending[position+1:], pending[position:])
pending[position] = item
}
}
}