feat(director): talk look-ahead buffer [spec 04]#26
Merged
Conversation
Add Brain.next_talks(ctx, count=2) -> list[str] (additive; next_talk/respond unchanged): StubBrain returns count canned beats, ClaudeBrain issues one query whose beats come back as a prompt-specified JSON array (the SDK's plain query has no output-schema). _parse_talk_batch splits the array, tolerates code fences / surrounding prose, caps to count, and degrades a malformed batch to a single beat so a bad response costs the look-ahead, never the segment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pec 04] One next_talks(2) call feeds two segments: beat 1 airs while beat 2's synth runs in the background (scheduled before beat 1's await so it overlaps synth on a concurrent backend; beat 1 still airs first via inline lock acquisition). The next talk segment plays from the buffer with no Brain/synth wait. The buffer is discarded when it goes stale — a talkback steer (predates the user turn) or a song going on air (the buffered beat was a consecutive-talk continuation) — and settled on shutdown. Fleshes out spec 04 §3.2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…arsing [spec 04] Replace the prompt-emits-JSON + string parser (parse_talk_batch: find[/rfind], code-fence stripping) with the harness tool seam already used for music discovery. next_talks now runs a terminal emit_talk_beats tool whose input_schema fixes the shape; the SDK delivers the model's call as a parsed mapping, which talk_tools.parse_talk_beats validates into the ordered beats — no free-text JSON to scrape. An empty batch (model never made the terminal call) falls back to one plain-text next_talk beat rather than skipping the segment into dead air. The claude-agent-sdk query has no output schema (only tool-input schemas), so the tool seam is the robust, codebase-consistent way to a typed, guaranteed-shape result. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
wine-fall
added a commit
that referenced
this pull request
Jul 15, 2026
Slice 2 shipped (PR #26) using a terminal harness tool (emit_talk_beats / parse_talk_beats in talk_tools.py) for structured batch output, but the spec still described the pre-refactor prompt-JSON + _parse_talk_batch approach that no longer exists in code. Rewrite §3.2 and acceptance §5.4 to the tool-seam design (same seam as music discovery, spec 03-01), and fix the stale "slice 2 (next build)" label now that both slices are merged. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
PR#3 of the cold-start work — spec 04 slice 2: talk look-ahead.
One
next_talks(2)Brain call produces two consecutive beats. Beat 1 airs whilebeat 2's synthesis runs in the background; the next talk segment then plays
from the buffer with no Brain call and no synthesis on its critical path —
the inter-segment latency (~Brain + ~TTS per gap) is gone.
Brain.next_talks(ctx, count=2) -> list[str](additive;next_talk/respondunchanged). Theclaude-agent-sdkqueryhas no output schema (onlytool-input schemas), so the beats come back as structured output via the
harness tool seam already used for music discovery: a terminal
emit_talk_beatstool (talk_tools.py) whoseinput_schemafixes the shape.The model calls the tool; the SDK delivers the call as a parsed mapping that
parse_talk_beatsvalidates — no free-text JSON to scrape. An empty batch(model never made the terminal call) falls back to one plain-text
next_talkbeat rather than skipping the segment into dead air.
task, aired next with no wait. Discarded when stale — a talkback steer
(predates the user turn) or a song going on air (the beat was a
consecutive-talk continuation) — and settled on shutdown.
Completes spec 04 (slice 1 = music prefetch shipped in #25; this is slice 2).
Implements specs/spec04/04-no-dead-air.md
Why
Removes the per-gap Brain+TTS wait between talk segments — the compounding half
of the cold-start work (music prefetch, #25, removed the ~45s first-song wait).
Design notes
beat 1's
await, so on a concurrent backend (remote) beat 2 overlaps beat 1'ssynth, not just its playback; on a lock-serialized backend (sidecar) beat 1
still grabs the lock first via inline execution.
intervening song both invalidate a pre-generated beat, so it's dropped and the
next segment regenerates against current context.
countis a call parameter (default 2), not a config knob (spec 04 §6 —deepen only if measurement shows a remaining gap).
Tests
count, malformed → single beat, empty → empty.
next_talksshape (stub returns 2 consecutive beats).next_talks(2); steer discards the buffer (nextsegment regenerates fresh); song-intervening discards it; shutdown settles it.
Peer review
codex (gpt-5.5, xhigh): 3 findings, 3 applied, 0 dismissed — (1) schedule
look-ahead synth before awaiting beat 1 (overlap synth on concurrent backends);
(2) discard the talk buffer when a song intervenes (a consecutive-talk beat is
stale post-song, and its hidden synth competed with the intro); (3) cap the
parsed batch to
count(an over-producing model must not inflate the buffer).(2) and (3) got regression tests; (1) is a concurrency-timing reorder covered
functionally by the batching test.
Verified live
One real
ClaudeBrain.next_talks(count=2)smoke returned a clean JSON array oftwo Chinese spoken beats (look-ahead ACTIVE, not degraded) — confirming the
real-model batch assumption the fakes can't prove. The deterministic parser /
buffer / invalidation are unit-tested; "does the model reliably emit N clean
beats" stays an eval-track concern.
Post-open re-architecture (review-driven)
The batch output originally came back as a prompt-specified JSON array parsed by
a string-munging
parse_talk_batch(extract outermost[...], strip codefences). On review that was replaced with the harness tool seam (the
codebase's sanctioned structured-output mechanism, spec 03-01):
emit_talk_beatswith a typed
input_schema, delivered as a parsed mapping — no text parsing.Second codex round on that change: 2 findings, 1 applied (empty-batch →
fall back to one
next_talkbeat, not a skipped segment; regression-tested) +1 resolved (the new
talk_tools.pywas untracked in the reviewed diff —committing it fixes it). Verified live again: the real model calls
emit_talk_beatsand returns 2 clean beats.AI coding brief
"brain text optimization" — one Brain call for two beats + parallel TTS +
a look-ahead buffer, so segments after the first air with no Brain/TTS wait.
Final slice of spec 04 (pulled forward for cold-start).
supports an output schema — it doesn't, so prompt-specified JSON);
countacall parameter with default 2, no config knob; discard-on-steer semantics.
Earlier line-by-line review of the loop set the bar for spin/deadlock-free
control flow, kept here.
only for tool inputs, not plain-query output) settled the JSON-vs-tool question
before any code — worth doing whenever "make the model return structured data"
comes up. A one-line kickoff note ("SDK query has no output schema; use
prompt-JSON or the tool pattern") would pre-answer it next time.
🤖 Generated with Claude Code