Skip to content

berd-call: realtime calls for agents: live stream, own voices, and Spokesperson controls - #397

Merged
johnmatthewtennant merged 49 commits into
mainfrom
jtennant/berd-call-shared-speech-rate
Oct 20, 2026
Merged

johnmatthewtennant merged 49 commits into
mainfrom
jtennant/berd-call-shared-speech-rate

Conversation

@johnmatthewtennant

@johnmatthewtennant johnmatthewtennant commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Berd Call's realtime (Expert-Spokesperson) calls only worked the way Berd's app drives them. An agent on the CLI, or in the Buzz Berd Call plugin, had four problems:

  • It heard the call too often or too late.
  • It could only speak through the Spokesperson's paraphrase.
  • It got tangled in handoff tracking it didn't need.
  • It couldn't switch the Spokesperson on or off mid-call.

This PR fixes all four, plus two playback problems found in live calls.

Two modes for realtime calls

  • Berd mode (the default, for --codex and --stream) hears the call as Berd's Expert does: a batch of every row since the last one, after each Spokesperson reply and at each handoff. berd-call message-spokesperson --re CURSOR [--resolves HANDOFF_ID]... TEXT is a directive the Spokesperson says, and it settles the handoffs it names once they're heard. speak is refused. Handoffs follow Berd's rules: reminders, renewal waits for them, and a provider expiry with one open ends the call.
  • Buzz mode (--stream live --allow-direct-agent-speech) is for the plugin. As soon as the user's words are transcribed, a user row appears. As soon as a reply is generated, before it's spoken, a spokesperson_generated row appears. These come from the new session messages user_transcribed and spokesperson_generated. speak --re CURSOR plays in the agent's own voice through the call's TTS backend. Handoffs still reach the agent, but none is held open.

Agents in their own voice

  • An agent's reply plays in the Spokesperson's playback slot once the host says it's ready for output (output_ready).
  • User speech or cancel stops a reply where it was heard. A host that stops playing it, such as when the speakers change, also stops it there.
  • Afterwards the Spokesperson gets a silent context note saying what the user heard, so it can follow up without repeating it.
  • speak --voice NAME [--language BCP47] speaks one reply in another voice of the same backend, so each agent can sound different. A new voice is built off the session loop, so audio never stalls. Siri voices with missing assets are listed as not installed.

Spokesperson controls

  • The menu bar has a Spokesperson checkmark. berd-call settings --spokesperson true|false does the same from the CLI.
  • Switching restarts the running call's session in the other mode. It keeps the control port, stream, mute state, input-during-TTS policy, and the agents' live voice. In a --stream live call, the Spokesperson comes back with --allow-direct-agent-speech.
  • The Spokesperson's voice sits under its checkmark. The agents' voice, speech rate and input-during-TTS setting are in the next section.
  • One speech rate applies to every voice. The Spokesperson keeps as close to it as it can (0.25 to 1.5×), and the menu shows Speech Rate: 2× (Spokesperson 1.5×) when they differ. --spokesperson-rate still gives the Spokesperson a speed of its own.

Reliability

  • A Spokesperson voice or rate change that the user talks over is retried once the call is idle, instead of being rejected.
  • Clearing the Spokesperson's input buffer no longer stalls microphone audio. Input before the clear is discarded, and input after it is kept. This used to crash realtime calls when audio devices changed.
  • In a conventional call, a host player that stops (switching from AirPods to the speakers) ends the speech as speech_interrupted where it was heard, rather than speech_failed.

The Berd app doesn't use these paths, so its behavior is unchanged.

Related issue

None found. The live stream follows OpenAI's GPT-Live client delegation.

Testing

Integration tests run against a fake realtime server and a fake OpenAI speech endpoint.

Modes

  • In Berd mode, a stale message-spokesperson comes back pending. One that acknowledges the latest input and names a handoff is admitted and said by the Spokesperson, and only the other handoff stays open.
  • In Buzz mode, the session renews after a handoff and a provider expiry with no handoff open, and a direct reply is admitted and spoken.
  • In a live stream, user rows already shown are left out of the agent's batch. A generated reply waits for the words it answers and is shown once.

Own voice

  • A reply is spoken with its exact text in the requested voice, and the Spokesperson is told what was heard without being asked to say it. An unknown voice gets not_admitted / invalid_voice.
  • A reply waits for output_ready: nothing is synthesized or reported for 500 ms without it. A mismatched or repeated output_ready is stale.
  • A reply that never gets output_ready fails after two seconds with speech_failed: output readiness timed out, and the next reply is admitted. One whose agents' voice changes between admission and output_ready still speaks in the voice it was admitted with.
  • User speech, cancel, or a stopped player interrupts a reply where it was heard, and the Spokesperson is told that much (or that none was heard).
  • A host that never confirms a drain gets speech_failed, and the session exits.

Controls

  • Turning the Spokesperson on restarts a call with the agents' current voice and speed, and adds --allow-direct-agent-speech for a live stream. Options only the other mode takes are dropped both ways, including when --allow-direct-agent-speech comes before other options.
  • settings --rate 2 sets the agents to 2× and the Spokesperson to 1.5×. A rate the agents' voice refuses changes neither. Starting a call with --rate 2.0 starts the Spokesperson at 1.5×.
  • The menu reflects the live call: the Spokesperson checkmark, a switch in progress, the voice sections, and the combined Speech Rate title.

Reliability

  • Talking over a voice change rolls back, and the change is retried once the call is idle.
  • Microphone audio keeps reaching the provider while it holds back confirmation of an input clear.
  • Microphone frames stay in order across an input clear: those sent before it, then the clear, then those sent after.
  • In a conventional call, a stopped player previously gave speech_failed: host audio output failed. It now gives speech_interrupted, and the next reply completes.

Live calls from the Buzz plugin (berd-call start --stream live)

  • The user's words and the Spokesperson's replies appeared in the channel as they were said.
  • Two agents speaking with speak --voice were heard in distinct voices.
  • Ticking and unticking Spokesperson switched the call between modes, with the agents kept at Siri en-US-F 2×, and the call stayed up throughout.
  • Choosing 2× showed Speech Rate: 2× (Spokesperson 1.5×), and berd-call status reported 2.0 and 1.5.

Screenshots

Before: a Spokesperson call on main

Berd Call menu on main in a Spokesperson call: one Voice and one Speech Rate, with no Spokesperson toggle

After: Spokesperson on, agents at 2×

Berd Call menu with the Spokesperson on, its voice under the checkmark, and Speech Rate 2× (Spokesperson 1.5×)

johnmatthewtennant and others added 9 commits October 19, 2026 15:44
An Expert driving a realtime call from the CLI should hear the
conversation the way a GPT-Live delegate does: only when the
Spokesperson hands off, with everything said since the last handoff.
Until now `start --stream` and `--codex` also flushed after every
Spokesperson reply, waking the Expert on each turn, while a host that
shows the call as it happens saw the user's words only once the reply
had played.

Bare `--stream` (or `--stream handoffs`) and `--codex` now hold rows
until a handoff. `--stream live` shows the conversation as it happens:
a `user` row as soon as the words are transcribed, and a
`spokesperson_generated` row as soon as a reply is generated, before it
is spoken and only once no user speech is still being transcribed. The
session emits these as `user_transcribed` and `spokesperson_generated`
previews; the transcript recorded after playback stays authoritative.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
With `--expert-voice tts`, an Expert-Spokesperson session builds the
configured TTS backend beside the realtime connection. Each reply plays
through that backend in the Spokesperson's playback slot instead of
being read aloud by the realtime model, so the same turn gates and
barge-in apply to both voices. Afterwards the realtime session gets a
silent note of what the user heard, and the handoffs the reply answers
settle once any of it was heard.

`session.tts` and `set_tts_settings` now describe the Expert's voice in
that mode, and every Expert-Spokesperson session reports the realtime
model and voice as `session.spokesperson`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
…pert's

With --expert-voice tts, the one voice and speed control changed the
Expert's voice, leaving no way to change the Spokesperson's during a call.

The Spokesperson now has its own controls in these calls:
- `set_spokesperson_settings` swaps in a replacement realtime session, the
  same way `set_tts_settings` changes the Spokesperson's voice elsewhere.
- `settings --spokesperson-voice` and `--spokesperson-rate`, plus matching
  `start` options, set its voice and speed.
- The menu bar shows Agent and Spokesperson voice and speed menus side by
  side, each limited to its own range of speeds.

`session.spokesperson` is now the Spokesperson's full revisioned OpenAI
snapshot. It appears only in calls whose replies have their own voice,
since elsewhere `session.tts` is already the Spokesperson's.

The Spokesperson's speed no longer follows the Expert's rate. The voice
and speed it was last given are saved, and come back in the next call
whose replies have their own voice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
`speak --voice NAME [--language BCP47]` speaks a single utterance in
another voice of the call's reply backend, keeping its other settings
and leaving the call's voice and saved settings alone. Agents sharing a
call can then sound distinct. A voice the backend doesn't offer, or one
for a call whose replies the Spokesperson speaks, is not admitted with
the new `invalid_voice` reason. Built backends are cached per voice
until the call's settings change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
sirittsd reports en-GB-C and en-GB-D as downloaded on a Mac where their
assets aren't on disk, and then speaks both in the same low-quality
substitute (byte-identical renders). Every voice that really speaks has
a TTSAXResourceManager contentPath; these have none. The catalog now
counts a voice as installed only when sirittsd reports it downloaded
and its content path exists, so the menu bar, Berd's settings and the
plugin stop offering it, and `voices download` fetches it instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
A reply whose playback failed without the host confirming the
cancellation left old audio possibly still playing, yet the session
went on admitting output. Carry the playback failure's quiescence into
the reply result and end the session after reporting the failure, as
conventional playback already does.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`speak --voice` built and validated a voice it hadn't used yet on the
session loop. Siri validation can wait seconds for sirittsd, so a
reply already playing could miss its audio acknowledgement deadlines
and microphone input could back up. Build the voice on a worker and
hand the request back to the loop once it's ready, or refuse it there
if the voice can't be built.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A speech request whose voice was still being built was invisible to
`cancel`, which answered stale; the request then came back and played.
Track the requests waiting on a voice, cancel them there, and drop a
cancelled one when its voice arrives.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The test host can now hold a drain and then keep serving, so a reply
stays playing until something stops it.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
@johnmatthewtennant
johnmatthewtennant marked this pull request as ready for review October 20, 2026 16:59
johnmatthewtennant and others added 2 commits October 20, 2026 13:15
An admitted Expert-voice reply was queued for playback at once, so it
could start before the parent sent output_ready. It now waits: a
matching output_ready marks it ready and queues it, and the accepted
result is written before playback can begin. Mismatched, repeated or
dropped requests are stale.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A failed Expert reply was treated as unheard, so its handoffs were
released and the Spokesperson was told the user heard none of it.
Playback failures now carry what had been played, so the heard prefix
reaches the Spokesperson and an answered handoff is dismissed.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>

@morgmart morgmart left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated code review

Request changes: the shared live rate update can commit the agents' rate before the Spokesperson update rejects or fails, leaving the voices and saved preference inconsistent even though the command reports failure. The supplied GitHub evidence is valid; Transcript virtualization and Tauri Rust checks (Linux) were still in progress when captured, while the other listed checks had passed.

Deterministic publication result: 1 blocking and 0 non-blocking inline finding(s) publishable; 0 duplicate(s) suppressed; 0 blocking screenshot-evidence requirement(s) in this review body.

Comment thread src-tauri/crates/berd-call/src/host_session.rs Outdated
johnmatthewtennant and others added 17 commits October 20, 2026 13:16
With --expert-voice tts, a reply could be dropped: one written before
the user's latest words came back pending, and one naming a handoff
already settled was not admitted. An open handoff also kept the
Spokesperson's session from renewing, and ended the call if the
provider expired it.

Such a reply is now always admitted, whatever its cursor, and settles
every open handoff as soon as it is, so --resolves is ignored. Open
handoffs no longer hold back renewal, recovery or a voice change, and
the replacement session is told what they asked so it doesn't hand
them off again. A reply that arrives during a replacement waits for
it rather than failing it. Calls whose replies the Spokesperson says,
and the Berd app, behave as before.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
In a call with --expert-voice tts, every speak played in the Expert's
voice, so an Expert couldn't have the Spokesperson tell the user
something instead. `berd-call message-spokesperson TEXT` (protocol:
`prepare_speak.relay`) hands the text to the Spokesperson to say, on the
same terms as an own-voice reply there: admitted however stale, and
answering every open handoff once admitted.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
`message-spokesperson` is how an Expert answers through the Spokesperson,
so it now works the same in a call without --expert-voice tts: admitted
however much the user has said since, and answering every open handoff,
with no --re or --resolves. `speak` there keeps today's rules.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
…nt-speech

In an Expert-Spokesperson call, the Expert now reaches the user through
the Spokesperson with `message-spokesperson`, and `speak` is refused
unless the call was started with --allow-direct-agent-speech. That flag
replaces --expert-voice tts. With it, a reply in the Expert's own voice
must acknowledge the latest input, as `speak --re` does everywhere, and
once admitted it still answers every open handoff. A message to the
Spokesperson stays admitted however stale.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
A `--codex` task heard a realtime call only at handoffs. It now gets a
batch after every Spokesperson reply and at each handoff, as Berd's
Expert and berd-call before the handoffs stream did, so it can follow
the conversation and answer what was said last. `--stream` (handoffs)
still holds rows until a handoff, and `--stream live` is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
`message-spokesperson` was admitted however stale and answered every
open handoff. It now takes the terms of a directive from Berd's Expert:
`--re CURSOR` is required and must acknowledge the latest input (the
Spokesperson's own speech doesn't count), or the message comes back
`spoke:false` with what was missed, and each `--resolves HANDOFF_ID`
settles that handoff once the Spokesperson has said it. This holds in
any Expert-Spokesperson call, including one with
--allow-direct-agent-speech, where `speak` still answers every handoff.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
`--stream` (or `--stream handoffs`) printed nothing in an
Expert-Spokesperson call until a handoff. It now prints the same
batches as `--codex`, after every Spokesperson reply and at each
handoff, so the CLI has two ways to hear a realtime call: as Berd's
Expert does, or as it happens with `--stream live`. `handoffs` is no
longer accepted after `--stream`.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
With --allow-direct-agent-speech the Spokesperson still hands off and
the Expert still hears each handoff, but nothing tracks it: no
reminders, no renewal or recovery held back, no note for a replacement
session, and nothing for --resolves to name. A reply is said once it
acknowledges the latest input, as before. Calls without the flag keep
Berd's handoff rules.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
The voice session read its arguments in pairs to find
--pcm-output-fd, which the host appends last. The valueless
--allow-direct-agent-speech shifted the pairs, so every call started
with it failed with "voice session event stream closed during
startup".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
A Spokesperson voice or rate change builds a new realtime session, and
if anyone spoke before it was ready, the change was rejected with
"Spokesperson conversation changed during voice replacement", so a
menu-bar change made mid-conversation never took. In a berd-call start
call it is now tried again once the call is idle, with the
conversation as it then stands, and reports one result when it applies.
The Berd app's handling is unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
OpenAI sends the Spokesperson's audio about as fast as it plays.
Playback sent it to the host only in whole 170 ms chunks and started
on the first one, so the speaker often ran dry before the next chunk
was complete, and any late delta left an audible gap.

Gather 300 ms of each reply before it plays, then send each delta as it
arrives. A test plays a reply arriving in real time with one 150 ms
stall: whole chunks leave a 109 ms silence; the lead plays through.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
On a live realtime call, in both modes, change the Spokesperson's
speed, then its voice, then the input policy, then mute and unmute,
and check each one lands. With a separate agent voice, also change the
agent's voice and speed. Check that the Spokesperson's realtime session
is rebuilt with each new voice and speed.

Run the retry test for a change the user overtook in both modes as
well. With a separate agent voice, the Spokesperson rate change it
retries fails without the retry. On the host, a Spokesperson voice or
rate change keeps the other setting and the revision, and is refused
when the call has no separate Spokesperson voice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
…dio"

This reverts commit aad01ee. Its premise was wrong: measured on six
live replies, OpenAI Realtime sends a reply's audio 3 to 5.5 times
faster than it plays, in 150 to 400 ms deltas, so a lead covers no
late arrival and only delays the first sound.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
The host reports a chunk played only when the output device has played
it: about 60 ms after it is heard on a MacBook's speakers, and a few
hundred more on Bluetooth headphones. Playback held at most 400 ms
beyond what was reported played (less 100 ms of delivery safety), so
on headphones the player ran dry while waiting for credit, and the
Spokesperson's speech, and an agent's, came out in short gaps.

Let the host hold a second beyond its reports, which plays through
reports up to about 800 ms late. A test plays 3 s of audio with
reports 300 ms late: the old limit leaves 1041 ms of silence; this
leaves none.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
When the host's player stops, as when the output device changes, the
host ends that speech and reports audio_failed. The session can still
send chunk, end or cancel records for it before hearing the report.
The host treated those as protocol errors and ended the call. It now
drops late chunk and end records for the speech it retired and answers
a late cancel with what it played.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
A Spokesperson reply whose host player stopped ended the whole call
with "host audio output failed". The session now ends that reply where
the host stopped, as if the user had interrupted it: the provider's
item is truncated to what played, the transcript is published as
interrupted, and the next reply gets a new player.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
Switching the output device restarts the microphone and resets the
Spokesperson's input. The realtime loop waited for the provider to
confirm the clear, so microphone audio stopped draining, the 32-frame
session queue filled, and the session failed with "session PCM input
queue is full".

Input resets and mutes now send the clear without waiting. The loop
answers them in order once the provider confirms, coalesces a request
that arrives while a clear is outstanding into one follow-up clear,
and holds a pending voice change until no clear is outstanding.
Resets share the input queue with microphone audio, so a clear
discards exactly the audio sent before it.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
johnmatthewtennant and others added 13 commits October 20, 2026 13:18
Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When the host's player stops, the Spokesperson playback worker sends
its result and stops taking audio. Provider audio that arrived in the
meantime hit the closed worker first and ended the call, before the
loop read the result that interrupts the reply where it was heard.
Drop that audio instead and let the result settle the reply.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The output_ready gate and failure tests restacked from below used the
reply's old optional text and started sessions with `--expert-voice
tts`. A call that lets the agent speak holds no handoff open, so the
failure tests now check only what the Spokesperson is told.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
When the speakers change mid-reply, macOS stops the audio engine and
the host reports its player failed. A conventional call reported that
as `speech_failed: host audio output failed`, so `speak` printed an
error even though the call carried on. Treat a quiescent host failure
like a barge-in: the speech ends as interrupted where it was heard.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A stopped host player now interrupts speech rather than failing it, so
the Expert reply tests from below check that the reply is interrupted
where it was heard and that the Spokesperson is told that much.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Spokesperson checkmark in the menu bar, and `berd-call settings
--spokesperson true|false`, restart the running call's session in the
other mode. Agents keep the voice they have now, options only the
other mode takes are left out, and a `--stream live` call gets the
Spokesperson back beside the agents' own voice.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
…oggle

Turning the Spokesperson on or off now restores the agents' live
speech settings in the new session, so an OpenAI voice and model chosen
during the call survive, which startup options can't express. When
agents have no voice of their own after the switch, their speed is left
out rather than becoming the Spokesperson's, whose range is narrower; a
conventional call at 2× no longer ends when the Spokesperson turns on.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The menu showed the call's mode only once the restarted session
answered, so the checkmark lagged a switch by the session's startup
time. The menu now shows the chosen mode until the switch finishes, and
keeps the last status while the restarting call doesn't answer.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With the Spokesperson on, its voice and speed now sit in its menu
section, under the checkmark, and the agents' voice, speed and
input-during-TTS setting get the next section.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`settings --rate`, and so the menu bar's Speech Rate, now also sets
the Spokesperson's speed in a call whose agents have their own voice,
as close as the Spokesperson can go (0.25 to 1.5). The menu shows one
Speech Rate, noting the Spokesperson's when it differs. At startup the
Spokesperson speaks as close to `--rate` as it can, instead of refusing
a faster rate, and turning it on keeps the agents' speed.
`--spokesperson-rate` still gives the Spokesperson a speed of its own.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
…efused

A rate the agents' voice rejects returned the Spokesperson's applied
result instead, and changed only the Spokesperson.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
@johnmatthewtennant
johnmatthewtennant force-pushed the jtennant/berd-call-shared-speech-rate branch from 9754301 to 8f9f63d Compare October 20, 2026 17:24
@johnmatthewtennant johnmatthewtennant changed the title berd-call: one speech rate for agents and the Spokesperson berd-call: realtime calls for agents: live stream, own voices, and Spokesperson controls Oct 20, 2026
@johnmatthewtennant
johnmatthewtennant changed the base branch from jtennant/berd-call-spokesperson-toggle to main October 20, 2026 17:25
@johnmatthewtennant
johnmatthewtennant requested a review from a team October 20, 2026 17:25
johnmatthewtennant and others added 5 commits October 20, 2026 13:27
A shared speech rate applied the agents' rate first and then the
Spokesperson's. If the Spokesperson refused, the agents kept the new
rate, and it was saved, while the command reported the failure. The
agents' previous settings are now restored, so a refused change leaves
both voices as they were.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A direct Expert reply admitted without the parent's output_ready
stayed reserved forever, so every later reply was refused. Give it
the two-second readiness deadline PROTOCOL.md sets for all admitted
speech: on expiry it fails with speech_failed, the Spokesperson is
told none of it was heard, and the slot is free again.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A direct reply in the Expert's usual voice took its TTS lease only
when playback began, so a set_tts_settings applied between admitted
and output_ready changed the voice of speech already admitted.
PROTOCOL.md says admitted speech keeps the configuration it was
admitted with; take the lease in submit_expert_prepare instead, as
per-utterance voices already are.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
merge_arguments reads flag-value pairs, so a call started with
--allow-direct-agent-speech before other options, such as
--mode expert-spokesperson --allow-direct-agent-speech --voice X,
misaligned every later pair when the Spokesperson was switched off,
and the restart failed validation. Remove the valueless flag first;
with_spokesperson already adds it back when the new mode wants it.

Signed-off-by: John Tennant <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>

@morgmart morgmart left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated code review

Engineering review is clear. The full prepared comparison was inspected statically, the supplied GitHub evidence is structurally valid and all listed check runs passed, and the prior atomic shared-rate concern is both addressed in the current implementation and protected by a resolved thread with a substantive human reply.

Deterministic publication result: 0 blocking and 0 non-blocking inline finding(s) publishable; 0 duplicate(s) suppressed; 0 blocking screenshot-evidence requirement(s) in this review body.

Pending checks: 1 check(s) are not complete.

This approval reflects the completed code review only; merge readiness remains governed by the repository's required checks.

@johnmatthewtennant
johnmatthewtennant merged commit 629e0af into main Oct 20, 2026
15 of 16 checks passed
@johnmatthewtennant
johnmatthewtennant deleted the jtennant/berd-call-shared-speech-rate branch October 20, 2026 18:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants