Skip to content

feat(director): talk look-ahead buffer [spec 04]#26

Merged
wine-fall merged 3 commits into
mainfrom
zachg-0714--talk-lookahead
Jul 15, 2026
Merged

feat(director): talk look-ahead buffer [spec 04]#26
wine-fall merged 3 commits into
mainfrom
zachg-0714--talk-lookahead

Conversation

@wine-fall

@wine-fall wine-fall commented Jul 15, 2026

Copy link
Copy Markdown
Owner

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 while
beat 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 /
    respond unchanged). The claude-agent-sdk query has no output schema (only
    tool-input schemas), so the beats come back as structured output via the
    harness tool seam
    already used for music discovery: a terminal
    emit_talk_beats tool (talk_tools.py) whose input_schema fixes the shape.
    The model calls the tool; the SDK delivers the call as a parsed mapping that
    parse_talk_beats validates — 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.
  • Look-ahead buffer in the Director: beat 2 buffered as a background synth
    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 airs first, always. The look-ahead synth tasks are scheduled before
    beat 1's await, so on a concurrent backend (remote) beat 2 overlaps beat 1's
    synth, not just its playback; on a lock-serialized backend (sidecar) beat 1
    still grabs the lock first via inline execution.
  • Staleness = discard. Freshness beats saving one call: an interjection or an
    intervening song both invalidate a pre-generated beat, so it's dropped and the
    next segment regenerates against current context.
  • count is a call parameter (default 2), not a config knob (spec 04 §6 —
    deepen only if measurement shows a remaining gap).

Tests

  • Parser: plain array, code-fence/prose tolerance, empty-item drop, cap to
    count
    , malformed → single beat, empty → empty.
  • next_talks shape (stub returns 2 consecutive beats).
  • Buffer: two segments from one next_talks(2); steer discards the buffer (next
    segment regenerates fresh); song-intervening discards it; shutdown settles it.
  • Full suite: 242 passed, 12 deselected.

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 of
two 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 code
fences). On review that was replaced with the harness tool seam (the
codebase's sanctioned structured-output mechanism, spec 03-01): emit_talk_beats
with 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_talk beat, not a skipped segment; regression-tested) +
1 resolved (the new talk_tools.py was untracked in the reviewed diff —
committing it fixes it). Verified live again: the real model calls
emit_talk_beats and returns 2 clean beats.

AI coding brief

  • Original request: Speed up murmur's first cold start. This PR is the
    "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).
  • Manual interventions: User chose JSON output (checked first whether the SDK
    supports an output schema — it doesn't, so prompt-specified JSON); count a
    call 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.
  • Retro: Checking the SDK's structured-output support up front (it has schema
    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

wine-fall and others added 2 commits July 15, 2026 10:45
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 wine-fall merged commit 70cc9cd into main Jul 15, 2026
5 checks passed
@wine-fall wine-fall deleted the zachg-0714--talk-lookahead branch July 15, 2026 07:17
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>
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