Skip to content

berd-call: capture calls and replay speech detection profiles - #426

Draft
johnmatthewtennant wants to merge 45 commits into
mainfrom
jtennant/quinn/call-learning-loop
Draft

johnmatthewtennant wants to merge 45 commits into
mainfrom
jtennant/quinn/call-learning-loop

Conversation

@johnmatthewtennant

@johnmatthewtennant johnmatthewtennant commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Berd Call needs recordings and repeatable measurements to tune speech detection from daily use. This adds device-specific listening and interruption profiles, opt-in local call capture, offline replay and evaluation of detection settings, and fixes to the speech hold that keeps agents from talking over the user.

Capture. berd-call start --capture DIR records microphone audio, sent agent audio, timestamped protocol events, per-frame VAD decisions, recognizer generations, the default input and output devices (including changes mid-call), and each Apple final's audio end with how many speech turns it resolved.

Replay and evaluation. berd-call vad-replay events CAPTURE_DIR measures request waiting against VAD and pending recognition, and the delay from the end of detected speech to each final transcript and each hold release. convert, extract and evaluate turn captures into grouped fixtures and compare static candidates against a baseline on held-out sessions. learning_evaluation::evaluate_corpus exposes an EligibleCandidate only for corpora with exact device evidence; today's captures remain diagnostic (trial_ineligible), so nothing is promoted automatically. detection_profiles::prepare_trial and finish_trial keep the saved baseline until a trial passes on the same devices and settings.

Speech hold. Agent speech waits while the user speaks and while Apple still owes a final. Previously each VAD turn added a pending credit and each final resolved one, so a sentence split by pauses left credits outstanding and nearly every hold ran the full pending-utterance timeout (55 of 57 holds across two recorded calls). The native bridge now passes each final's audio end, and a final resolves exactly the ended turns it covers; a final for an earlier phrase of a still-pending turn resolves nothing. Apple can take seven seconds or more to finalize a second sentence. So the bridge also reports Apple's interim results, and while its latest result is interim words rather than a final, the hold lasts up to ten seconds past the timeout. Noise Apple never transcribes keeps the timeout. When the hold does expire, recognition keeps running and late finals are still published. Empty recognition results no longer raise detection thresholds.

Related issue

Consolidates #421, #422 and #424. No related issue found.

Testing

  • cargo test -p berd-call passes locally on macOS at e173655 (400 library and 284 binary tests plus every integration target), including regressions for multi-turn and split-phrase finals, holds that wait for interim words and holds that keep their timeout once a final follows them, cancellation while live playback waits for provider audio (with buffered tails of 1 and 4095 samples), and capture route recording.
  • cargo test -p berd-call --lib final_end_is_measured_on_the_pushed_audio_timeline -- --ignored runs real on-device SpeechTranscriber over say output with 1 s and 3 s of leading silence. Final ends shift by the lead within 40 ms and land about 0.3 s after the last sound, which confirms finals report the same timeline the session pushes.
  • On recorded calls, vad-replay events shows finals arriving a median 1.8 to 2.6 s and at p90 3.1 to 4.1 s after speech ends, while holds ran 5.0 s.
  • A live call on 43efb0e had a median hold of 2.40 s, with 1 of 14 holds at the 5 s ceiling. In that one, the second sentence finalized 3.8 s after the hold released, and an agent's reply started and was cut off.
  • Replaying that call's microphone audio in real time through e173655: no final arrived after its hold released, and that hold lasted 8.7 s. Replaying a 20-minute call: median hold 2.73 s, max 5.06 s, the same as before the interim-words change.

Automatic promotion from captured calls is not implemented: captured corpora lack verified playback provenance and labels, so evaluate_corpus returns no eligible candidate.

Gigi and others added 3 commits October 22, 2026 09:20
Signed-off-by: Gigi <65cc00ce5da00abcd27be4a87ad29d7ef8431766d769a24d94f004fb8fe07066@buzz.block.builderlab.xyz>
Signed-off-by: Gigi <65cc00ce5da00abcd27be4a87ad29d7ef8431766d769a24d94f004fb8fe07066@buzz.block.builderlab.xyz>
Co-authored-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>

Co-authored-by: Damon <8a08290d889f1c68921542bfe9631d4cd671be9bea5f1114951f386819e4c10a@buzz.block.builderlab.xyz>
Signed-off-by: Quinn <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Quinn and others added 13 commits October 22, 2026 09:30
Signed-off-by: Quinn <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Signed-off-by: Quinn <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
With --capture and Apple recognition, vad.jsonl now has one row per
16 ms detector frame: its probability, threshold, mode, detection and
gate output, placed in mic.wav by mic_sample (the sample just past the
frame's audio, corrected for resampler lag), with settings records
before the rows that use them. events.jsonl gains recognizer records
for each generation and why it began or ended, since a new generation
also resets the speech gate.

Capture times no longer go backwards: producers on several threads can
enqueue slightly out of order, so the writer never writes an earlier us
than the last, gap records use the latest written time, and event frame
counts come from the frames actually written.

Co-authored-by: Damon <8a08290d889f1c68921542bfe9631d4cd671be9bea5f1114951f386819e4c10a@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: John Tennant <jtennant@block.xyz>
Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
Signed-off-by: Quinn <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Signed-off-by: Nora <49aa1f65411fd096d2e2ec144f1e7aa36fdc76d1b907cfdf7be000c66f9d3b8e@buzz.block.builderlab.xyz>
Signed-off-by: Gigi <65cc00ce5da00abcd27be4a87ad29d7ef8431766d769a24d94f004fb8fe07066@buzz.block.builderlab.xyz>
Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
Signed-off-by: Gigi <65cc00ce5da00abcd27be4a87ad29d7ef8431766d769a24d94f004fb8fe07066@buzz.block.builderlab.xyz>
Signed-off-by: Gigi <65cc00ce5da00abcd27be4a87ad29d7ef8431766d769a24d94f004fb8fe07066@buzz.block.builderlab.xyz>
…tures

Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
…vation

Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
Quinn and others added 7 commits October 22, 2026 12:34
Signed-off-by: Quinn <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Signed-off-by: Quinn <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Add failure_fixtures and `vad-replay extract CAPTURE_DIR OUT_DIR
--partition train|held-out`. Captures are cut into short clips around
recorded fragments, interruptions, held replies and disagreements with
human-checked labels. Findings are review hints, never labels.

Clips never touch a gap, limit, lost mic frames, missing detector rows
or trace beyond mic.wav, and they start only where a fresh replay gate
equals the live one: the capture start, a recognizer or mute reset, or
a quiet run whose bounds are recorded so candidates outside them can be
rejected. Replay inputs are clip-relative, with a 48-sample-aligned
source offset, a recut-stable source session and the partition in
provenance. Speech labels are never truncated at onset, and label
provenance is preserved.

Co-authored-by: Damon <8a08290d889f1c68921542bfe9631d4cd671be9bea5f1114951f386819e4c10a@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: John Tennant <jtennant@block.xyz>
Signed-off-by: Quinn <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
Captures now record the default input and output device UIDs at
session start, and fixture provenance carries them with an explicit
session_start basis. learning_corpus::load reads fixture directories
into one corpus per device pair, with index-aligned inputs, source
sessions and start constraints for evaluate_grouped.

Each fixture's metadata, replay input and decoded mic.wav length must
agree with its provenance; captures without UIDs are rejected. A
source whose fixtures overlap, mix partitions or mix setups is
rejected whole. File sizes, fixture count and total bytes are bounded.

Every corpus lists why it cannot authorize a live profile trial: the
host can switch profiles mid-call, so a start-time setup does not
establish the devices behind each clip.

Co-authored-by: Damon <8a08290d889f1c68921542bfe9631d4cd671be9bea5f1114951f386819e4c10a@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: John Tennant <jtennant@block.xyz>
Signed-off-by: Quinn <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
johnmatthewtennant added a commit that referenced this pull request Oct 22, 2026
…#425)

## Summary

Prevent native status audio operations from blocking the microphone PCM
loop. Keep cues on their five-second schedule, skip cues due during
speech, and let a cue already playing finish. A Working/Waiting
transition during speech is announced after speech ends.

Status cues follow the existing mute-during-TTS setting, with Auto
resolved against the selected output. Cue suppression pauses incoming
PCM without discarding active speech or pending recognition.

### Related issue

Supersedes #423. Optional VAD debug
playback and its controls belong to
#426; this PR does not advertise or
implement them.

### Testing

Retained regressions pass for skipped ticks without catch-up, active
sample completion, deferred status announcements, microphone admission
while an audio worker is stalled, recognition-epoch preservation and
selected-output Auto policy. A compatibility regression loads
preferences from a debug-enabled build, saves and reloads them, verifies
unrelated nondefault values survive, and verifies the optional debug key
is omitted.

The rebuilt settings help omits the debug flag while retaining status
and input-policy controls. The full package run still reports the
previously observed Spokesperson timing/fixture, CLI usage and
session-protocol failures. Normal push checks pass. Focused removal
review reports no findings. Installed call and plugin files are
unchanged.

---------

Signed-off-by: Gigi <65cc00ce5da00abcd27be4a87ad29d7ef8431766d769a24d94f004fb8fe07066@buzz.block.builderlab.xyz>
Signed-off-by: Nora <49aa1f65411fd096d2e2ec144f1e7aa36fdc76d1b907cfdf7be000c66f9d3b8e@buzz.block.builderlab.xyz>
Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
Co-authored-by: Gigi <65cc00ce5da00abcd27be4a87ad29d7ef8431766d769a24d94f004fb8fe07066@buzz.block.builderlab.xyz>
Co-authored-by: Nora <49aa1f65411fd096d2e2ec144f1e7aa36fdc76d1b907cfdf7be000c66f9d3b8e@buzz.block.builderlab.xyz>
Co-authored-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
Quinn and others added 2 commits October 22, 2026 13:23
Signed-off-by: Quinn <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
Zack and others added 4 commits October 22, 2026 13:41
Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
The host now sends AudioRoute records through the session's ordered
input. input_opened names the microphone UID once its stream plays and
before any of its frames are forwarded. output observes the default
output UID and a CoreAudio change-notification count, before the first
frame, on change and every 5 seconds, only when the menu bar's main run
loop can deliver notifications.

Extraction gives a clip the observed_route basis only within one input
stream and between agreeing output observations, with no possible
record loss overlapping that span. Other clips in routed captures are
excluded. Output stays observational, so corpora remain diagnostic
only; VAD_REPLAY.md records what still prevents trial eligibility.

Co-authored-by: Damon <8a08290d889f1c68921542bfe9631d4cd671be9bea5f1114951f386819e4c10a@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: John Tennant <jtennant@block.xyz>
Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
@johnmatthewtennant
johnmatthewtennant force-pushed the jtennant/quinn/call-learning-loop branch from f0f55e5 to 5f7db75 Compare October 22, 2026 18:18
Quinn and others added 7 commits October 22, 2026 14:19
Signed-off-by: Quinn <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
The playback worker checks the cancellation flag under the state lock
and then waits on the condvar. A cancel request that stores the flag
and notifies between those two steps is lost, so the worker sleeps
until its operation timeout. This reproduces the CI timeout in
autonomous_spokesperson_cancel_is_correlated_before_audio_cancellation_and_then_stale.

Signed-off-by: Henry <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
notify_cancel_requested stored the flag and notified without the state
lock, so a worker that had checked the flag but not yet waited missed
the wake and slept until its operation timeout. Taking the lock before
notifying orders the wake after any such check.

Signed-off-by: Henry <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Buzz plays and records through the system default devices, which can
change mid-call without the session hearing about it. A capture now
polls the default input and output every 250 ms and writes a route
event with their UIDs and names at start and on every change, so each
clip can be attributed to the devices in use around it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Signed-off-by: Henry <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Automatic tuning needs one entry point that combines replay acceptance
with the corpus's trial eligibility, so callers cannot promote settings
the CLI would report as diagnostic only. evaluate_corpus returns a
CorpusEvaluation whose eligible_candidate is Some only when replay
accepted a candidate, the corpus has no trial limitations, both device
identities are known and the baseline is static. The CLI now reports
eligibility through the same method.

Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
…file trials

An empty recognition result cannot tell noise from a recognizer failure,
yet the live tuner raised the detecting mode's threshold on each one. A
run of recognizer failures could therefore teach the detector to ignore
speech. No-result outcomes are now counted but never adjust thresholds.

detection_profiles gains prepare_trial and finish_trial. A prepared
trial holds an eligible candidate in memory with the saved baseline it
expects, and only a passed trial on the same devices with the applied
settings promotes through the existing compare-and-save. Failed or
abandoned trials leave the saved profile untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Signed-off-by: Damon <8a08290d889f1c68921542bfe9631d4cd671be9bea5f1114951f386819e4c10a@buzz.block.builderlab.xyz>
@johnmatthewtennant
johnmatthewtennant force-pushed the jtennant/quinn/call-learning-loop branch from d1237d5 to 18d00ed Compare October 23, 2026 03:26
Henry and others added 5 commits October 22, 2026 23:27
Each VAD turn adds a pending recognition credit, but SpeechTranscriber
emits one final for a sentence that pauses split into several turns.
Resolving one credit per final left the rest outstanding, so in John's
recent calls 55 of 57 holds ran the full pending-utterance timeout even
though Apple had finished 0 to 4 seconds after he stopped.

Pass each final's audio end through the native bridge and resolve
exactly the ended turns whose speech it covers. A final for an earlier
phrase of a still-pending turn resolves nothing, so the rest of the
sentence keeps the hold. Finals without an end keep the old behaviour.
Captures record each final's end and how many turns it resolved.

Signed-off-by: Henry <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`vad-replay events` now reports how long after detected speech each
final transcript and each recognition hold release arrived. Across
John's three recent captures, finals came a median 1.8 to 2.6 s and at
p90 3.1 to 4.1 s after speech, while holds ran the full 5 s timeout.
These distributions bound how short the pending-utterance timeout can
safely be for a device.

Signed-off-by: Henry <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cancellation revoked output authority and notified output waiters, but a live playback worker could already be waiting for its next provider message. Queue Finish as well so the existing cancellation path can serialize Cancel without another provider event. A full input queue already gives the worker work that observes the revoked authority.

The regression synchronizes after one chunk and checks cancellation with no tail, one buffered sample and a nearly full partial chunk.

Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
…inalize

The hold lifted at the pending-utterance timeout even when Apple was still
transcribing. On a live call a second sentence took about seven seconds to
finalize, so an agent's reply to the first sentence started playing and was
then cut off by the late transcript.

The bridge now reports Apple's interim results. While interim words exist
past the latest final, the hold lasts up to ten seconds beyond the timeout,
until their final arrives; noise with no interim words keeps the timeout.
Empty finals now reach the host so they settle their audio. Captures
record interim coverage (`heard`) and Apple's finalization point
(`settled`).

Signed-off-by: Henry <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaying a 20-minute call held agents for the full fifteen seconds
twice after finals that had already arrived. Interim results end where
Apple's audio does, not at the last word, so they ran past the final's
end by more than the coverage slack and looked unfinalized.

Apple re-reports words a final leaves out as a fresh interim result, so
the hold now waits only while its latest result is interim words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Henry <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Zack and others added 2 commits October 23, 2026 08:30
Linux and Windows clippy flagged `heard` and `unfinalized` as never
read, because only the macOS recognizer reads them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
… call

Make the interim-words extension a setting (`interim_words_wait_ms`,
default 10000, replacing the hard-coded 10 s grace) and let four more
parameters learn online, each behind its own opt-in that defaults to
false so saved profiles load unchanged. A learned value is saved to the
detection profile with the thresholds.

Every parameter uses one rule: an error steps it by U toward fixing the
error, and every Kth success since its last step steps it back by U, so
it settles near one error in K + 1 outcomes. Steps stay inside the
learning bounds and a value outside them never moves.

- pending timeout (+250 ms, 2000..8000, K 19): a final with words
  arrives after the hold expired on the idle timeout; success is a final
  that resolves the hold before it expires.
- interim words wait (+500 ms, 2000..20000, K 19): a final arrives after
  the hold expired during the extension; success is a final that
  resolves it within the extension.
- VAD release (+32 ms, 160..1500, K 19): one final that resolves several
  ended turns has an agent start inside its words, read from the first
  word start Apple now reports before each final; success is such a
  split final the agent did not jump into.
- thresholds: keep the missed-detection step down, and step 0.01 back up
  after 99 detected-speech transcripts per mode.

Expose the opt-ins as `--dynamic-pending-utterance-timeout`,
`--dynamic-interim-words-wait` and `--dynamic-release`, the wait as
`--interim-words-wait-ms`, and per-parameter adjustment counts in the
detection snapshot. `DetectionTuner::without_drift` keeps errors only,
for replay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
@johnmatthewtennant
johnmatthewtennant force-pushed the jtennant/quinn/call-learning-loop branch from 6eec741 to 205d410 Compare October 23, 2026 13:37
johnmatthewtennant added a commit that referenced this pull request Oct 23, 2026
…cel fixes (#429)

## Summary

Berd Call kept holding the agent's reply after the user had finished,
and showed the user's words only once Apple finalized them. This
collects the release-safe fixes from the call learning work, without its
capture, replay or tuning infrastructure.

**Stuck speech holds.** The hold that keeps an agent from speaking over
the user is released by Apple's finals, but Apple emits one final for a
sentence that pauses split into several VAD turns. Resolving one turn
per final left the rest outstanding, so in recent calls 55 of 57 holds
ran the full 5 s timeout even though Apple had finished 0 to 4 s after
the user stopped. Expiry also cancelled the recognizer, throwing away a
transcript still on its way, and the hold lifted at the timeout while
Apple was still transcribing, so a reply started and was then cut off by
the late transcript.

- Expiry releases only the hold. Recognition, its queued audio and the
recognizer generation continue, so a late final still reaches the
conversation.
- Each final carries the end of its audio through the Swift bridge and
resolves exactly the ended turns it covers. A final that arrives during
the silence window, before its turn ends, resolves that turn when it
ends. A final for an earlier phrase of a turn still being spoken
resolves nothing.
- The bridge reports Apple's interim results. While Apple's latest
result is interim words, the hold lasts up to 10 s beyond the 5 s
timeout, until a final follows. Noise with no interim words keeps 5 s.

**Interim words.** Conventional macOS sessions emit `user_interim`
(`--stream` carries it as an ephemeral `0\tuser_interim` row) with
Apple's interim text, replaced by each update and cleared on its final,
mute, input reset or restart. It is never stored, never takes a token
and never interrupts output.

**One VAD threshold.** Listening and interruption during agent playback
use the same speech probability, 0.9, instead of separate thresholds.

**Playback and cancel fixes.** A cancel wake could be lost between a
playback worker's flag check and its wait, idle live playback did not
wake on cancellation, and handoff terminal failures lost their order.

### What came from where

| Commit | Source |
| --- | --- |
| wake idle live playback on cancellation | `80b97e4c2` in #426
(cherry-pick) |
| cover a cancel wake racing the flag check (test) | `f6097e517` in #426
(cherry-pick) |
| order cancel wakes after the flag check | `14943c91e` in #426
(cherry-pick) |
| preserve ordered handoff terminal failures | `6b5afdf00` in #426
(cherry-pick) |
| tolerate audio replies after child shutdown (test) | `46fdf603b` in
#426 (cherry-pick) |
| validate partial audio EOF after intentional disconnect (test) |
`71214694e` in #426 (cherry-pick, conflict resolved by dropping the
capture-only replay test) |
| expire speech holds without cancelling recognition | `2eda9918c` in
#426 (ported) |
| release the speech hold when a final covers its turns | `43efb0e00` in
#426 (ported) |
| keep the speech hold while Apple still has words to finalize |
`09ee43fd9`, `e173655ac`, `4d2f03488` in #426 (ported, final combined
behavior) |
| stream the user's words while they are transcribed | `7b28c3ecd` in
#428 (ported) |
| ignore interim words in native voice conversations | `e347e12a6` in
#428 (cherry-pick) |

The ports differ from #426 only where #426 depended on infrastructure
this branch leaves out:

- The pending-utterance timeout is a fixed 5 s constant and the interim
grace a fixed 10 s, not `DetectionSettings` fields, CLI flags or a
protocol bump.
- Capture hooks and the capture-only `heard` and `settled` records are
gone, along with the detection-evidence handling in finals
(`result_is_current` still gates hold accounting).
- A turn's end is the pushed audio minus main's 1.2 s silence flush
window, where #426 subtracted its configurable `release_ms`.

### Left in #426

Capture and replay, VAD debug cues, failure fixtures, speech-profile
evaluation, and the learning and tuning work (dynamic timeouts,
thresholds and release).

### Related issue

Follows #426 and #428. None found otherwise.

### Testing

- Real Apple recognition: the ignored on-device test
`mac_speech::tests::final_end_is_measured_on_the_pushed_audio_timeline`
passes, so a final's end is on the same pushed-audio timeline the VAD
turns use. That is what makes the coverage rule sound.
- Hold release, red/green:
`mac_final_before_its_turn_ends_releases_the_hold_when_it_does` fails
before the coverage fix (the turn stays pending until the 5 s timeout)
and passes after it. Companion tests cover a final resolving every turn
its audio covers, an earlier-phrase final keeping the hold, the hold
waiting for interim words and returning to 5 s once a final follows, and
late finals surviving hold expiry.
- Interim words: tests cover the stream through mute, reset and restart,
and the interim token matching the next final.
- Playback cancellation: the cherry-picked `session_protocol`
regressions run the real `berd-call` child against a scripted audio
host.

Not yet exercised on a live call: the end-to-end hold timing and interim
captions with a real microphone.

---------

Signed-off-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
Signed-off-by: Henry <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Signed-off-by: Aaron <f425b86c3b780d43f8d9cf1802429d04dd985bf92eac44fca9468b5ad3887c5e@buzz.block.builderlab.xyz>
Co-authored-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
Co-authored-by: Henry <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Aaron <f425b86c3b780d43f8d9cf1802429d04dd985bf92eac44fca9468b5ad3887c5e@buzz.block.builderlab.xyz>
Main now carries the speech-hold, interim-word and playback-cancel
fixes from #429, which this branch and #428 already had in their own
form, plus the Swift link-path fix from #430.

Conflicts take this branch's versions, with #428's interim words and
the final_end coverage fix layered on, so the configurable and learned
thresholds stay in place of #429's single fixed VAD threshold. The
swiftbuild link-path change from #430 is kept as is. The merged tree
matches the combined build that passed the full berd-call suite, plus
the two build.rs changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Henry <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>
@johnmatthewtennant
johnmatthewtennant force-pushed the jtennant/quinn/call-learning-loop branch from d5af7d3 to 47000de Compare October 23, 2026 19:46
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Henry <a0d9f90f86da5f023099d22b8e3599af162d9423a422d8f385a12815de879cf2@buzz.block.builderlab.xyz>

This branch has not been deployed

No deployments
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.

1 participant