Enable isolated Remote speech and protect reading exports

Attach incoming software speech to reader sessions with lease checks, local output priority and isolated cancellation. Preserve every document fragment source for privacy checks and export reviewed browse current-line reading, spelling and phonetic output.

Verified focused reading and Remote race tests, complete review and external Remote race suites, private relay integration, speech regressions, affected-package vet, reader build and Tentacles make check. Audible speech, physical input/Braille and live interoperability remain unverified.
This commit is contained in:
ak47 committed 2026-11-05 15:41:08 -05:00
1 parent ac67236eca
commit 49be8309fe
27 files changed
+986 -54

No files matched your search

+8 -4
View File
@@ -20,6 +20,9 @@ import (
type RemoteSpeechDelivery struct {
Context context.Context
Lease tentaclewire.RemoteLease
// Received is the reader's monotonic admission time of the first frame.
// Local output can discard old mailbox/assembly work before it is dequeued.
Received time.Time
// Speeches is one complete effect/utterance, including all IPC fragments.
// The owner applies priority once and must not deliver a partial assembly.
Speeches []tentaclewire.RemoteSpeech
@@ -30,13 +33,14 @@ type RemoteSpeechDelivery struct {
type remoteSpeechBatch struct {
id uint64
received time.Time
speeches []tentaclewire.RemoteSpeech
bytes int
}
// EnableRemoteSpeech installs one bounded mailbox, explicitly from reader code.
// Without an owner, remote.present is unavailable. No production speech worker
// is bound yet. The owner selects Session.Done as well as this channel.
// Without an owner, remote.present is unavailable. The owner selects session
// shutdown as well as this channel and joins its backend cleanup.
func (session *Session) EnableRemoteSpeech() <-chan RemoteSpeechDelivery {
session.mutex.Lock()
defer session.mutex.Unlock()
@@ -103,7 +107,7 @@ func (session *Session) remote_present_locked(params json.RawMessage) (any, *RPC
if request.Utterance != 0 && request.Utterance != request.Sequence {
return nil, &RPCError{Code: -32602, Message: "Invalid utterance assembly"}
}
batch = &remoteSpeechBatch{id: request.Utterance}
batch = &remoteSpeechBatch{id: request.Utterance, received: time.Now()}
} else if request.Utterance != batch.id || request.Speech.Priority != batch.speeches[0].Priority {
// No interleaved speech, cancel, pause, or changed priority may expose
// an incomplete utterance. Heartbeats may still advance the sequence.
@@ -125,7 +129,7 @@ func (session *Session) remote_present_locked(params json.RawMessage) (any, *RPC
}
access.speechBatch = nil
var released sync.Once
delivery := RemoteSpeechDelivery{Context: access.ctx, Lease: lease, Speeches: batch.speeches, Release: func() {
delivery := RemoteSpeechDelivery{Context: access.ctx, Lease: lease, Received: batch.received, Speeches: batch.speeches, Release: func() {
released.Do(func() {
session.mutex.Lock()
defer session.mutex.Unlock()
+4
View File
@@ -178,6 +178,7 @@ func TestRemoteSpeechSDKAtomicUtterance(t *testing.T) {
if _, code := remoteCall(t, session, "remote.present", request); code != 0 {
t.Fatal(code)
}
firstAdmitted := time.Now()
request.Sequence++
if _, code := remoteCall(t, session, "remote.heartbeat", request.RemoteOperation); code != 0 {
t.Fatal("heartbeat could not run during assembly", code)
@@ -210,6 +211,9 @@ func TestRemoteSpeechSDKAtomicUtterance(t *testing.T) {
t.Fatal(code)
}
delivery := <-deliveries
if delivery.Received.IsZero() || delivery.Received.After(firstAdmitted) {
t.Fatal("finalization refreshed the original assembly admission time")
}
if !reflect.DeepEqual(delivery.Speeches, frames) || delivery.Lease != lease || delivery.Context.Err() != nil {
t.Fatal("assembly changed semantics or ownership")
}