feat(voice,web): Luke's replies wait on no earlier acknowledgment, record write, or second settle, and a call greets from its startup history
Implements most of the plan "Make GPT Live voice more responsive": it takes Luke-specific waits off the path between a backend answer and GPT Live. It does not claim to fix slow barge-in. That cause is still open, and nothing changed in the mic or playback path.
What changed
-
Append channel (
packages/voice/src/live-session/append-channel.ts):queue()replacesenqueue/send/deliver.- Appends go on the wire in queue order, and each one's acknowledgement or error is tracked separately. No append waits on an earlier one's acknowledgement.
- The Live docs say an acknowledgement arrives when the timeline reaches the end of that context. So waiting on each one in turn held every later sentence, note and instruction behind the speech before it.
- A refused chunk is retried once under a fresh id. An unanswered one is reported and not retried.
- A chunk still queued when a Stop or silence lands is withdrawn unsent (new
APPEND_OUTCOME.WITHDRAWN).
-
Voice service (
live-session-service.ts):- A run's events are no longer deferred while its ask's record attach is pending.
- The service's own actions-settled gate is removed.
projectTurnEventsalready tells no sentence ahead of unsettled calls, and the brain still emits the mark. - A background (woken) run's reply no longer waits for the asked run to end.
- Stale-result handling is unchanged: newer delegations and Stop still silence older runs.
-
Live brain (
apps/web/server/voice/live-brain.ts): events reach the voice before the told-checkpoint is written. The write follows on one ordered writer in the socket's scope, which is drained when the scope closes. A crash between the telling and the write may repeat an update on re-attach. Within a running session nothing repeats, because positions are tracked in memory. -
Greeting:
planningOpeningHistory()is now the last developer message of the startupinput, which is the Live guide's greet-first recipe. The budget for that message is reserved before the history is trimmed. The relay's instruction → ack → cue aftersession.startedis removed, along withgreetingCue, theGREETING_*log events andgreetingTimeoutMs. A re-attach creates nothing, so it adds no greeting. -
eve's live stream wakes the follow (the developer approved this in the PR):
EveSessions.stream(sessionId)readsGET /eve/v1/session/:id/stream?startIndex=-1as{ type, turnId }per event. A refused stream comes back empty.- Auth change: the planning channel's
deploymentActormay now GET a session's stream. The door'sreachesSessionownership check still applies. The coder channel's actor and a POST to the route are still refused. - The live brain reads the record immediately on each non-delta event. It reads once more 60 ms later, because eve's store hook writes the row just after the event. The 250 ms poll stays as the fallback.
- The stream is only a wake signal: what Luke is told is still read from the record, with the same projection, order and numbering. If eve refuses the stream (for example before the session's first event is recorded) or drops it, delivery is no slower than today.
Not in this PR
- No change to question pacing, caption settling, speaking-state timers, mic settings or backend cancellation.
Testing
- New voice tests:
- consecutive sentences all go out before any acknowledgement, and an out-of-order ack settles only the append it names;
- refused-twice and unanswered handling;
- wire order holds when sends are slow;
- Stop withdraws queued chunks;
- the reply and next question are said while the record write is stalled;
- no settled mark is needed;
- a worker's finding is said during an asked run and silenced by the next ask.
- New web tests:
- with the poll pushed out to a minute, a turn's sentences and end still arrive the moment the stream reports an event, and a delta event wakes nothing;
- the eve client parses stream lines from the tail, skips junk and lease lines, and returns nothing for a refused stream;
- the door admits the deployment's stream read only for the account's own recorded session, and refuses another account's session, an unrecorded session, and a POST;
- events still reach the service while the told-checkpoint write is stalled, and the row catches up afterwards;
- a long earlier-call history still leaves the opening last and within the token and message bounds;
- a re-attach creates no opening.
pnpm check:affectedpasses.- Not verified: that the greeting is actually spoken from startup history, and how overlapping speech, Stop, reconnect, queued questions and background findings behave on a real call. These need a manual pass on a Mac (
./scripts/verify.shor./scripts/run.sh). CI builds nothing for the Mac.