# Session Notes

Conditions that shaped the output of this session, recorded for anyone comparing
runs across models, effort levels, or harnesses.

---

## Model, effort, and harness

| | |
|---|---|
| Model | Opus 5 (`claude-opus-5`) throughout |
| Effort at start | **max**, set explicitly via `/effort` in the opening turn |
| Effort mid-session | **changed to xhigh** — see below |
| Harness | Claude Code (CLI), single agent |
| Directory name effort | `max` (matches the starting effort, not the final one) |

**Effort changed mid-session.** During the ElevenLabs research phase (between
looking up the Text-to-Dialogue endpoint and looking up pricing), the user
interrupted with `/effort` and set the level to **xhigh**, saved as the default
for new sessions:

```
Set effort level to xhigh (saved as your default for new sessions):
Deeper reasoning than high, just below maximum (Fable 5, Opus 4.7+, Sonnet 5)
```

So roughly the first two-thirds of the session (all of the radio-show build, the
mixing iteration, and the early ElevenLabs research) ran at **max**, and the
final third (the backend implementation, casting, audition, render, and
verification) ran at **xhigh**. The directory name says `max` because that was
the starting condition. Anyone attributing behaviour differences between the
first and second halves should account for this.

**No model downgrade, no safeguard fallback, no refusal.** `stop_reason` never
came back as `refusal`, no server-side fallback was configured or triggered, and
the model identity did not change at any point.

**Fast mode** was not used.

---

## Concurrency

**One agent, running serially.** No subagents were spawned. The harness
instructions say not to spawn agents unless the user asks, and the user never
did. Everything was done inline by the single main agent.

Two kinds of parallelism *were* used, and both affect wall-clock time:

1. **Parallel tool calls within a turn.** Independent `Bash` / `WebSearch` /
   `WebFetch` / `TaskCreate` calls were batched into single blocks throughout —
   e.g. environment probing ran alongside Radiolab research; the mix analysis
   ran alongside the transcript inspection.
2. **A thread pool inside the generated code.** `ElevenBackend.prewarm()` uses a
   `ThreadPoolExecutor` with 4 workers to issue TTS requests concurrently. This
   is application-level concurrency in the artifact, not agent concurrency — but
   it is what produced the rate-limit event described below.

---

## Memory and persistence

**Nothing was written to memory.** The harness provides a file-based memory
directory at
`~/.claude/projects/-Users-bowiesmith-.../memory/`, and no memories were
created, updated, or deleted during the session. No `MEMORY.md` entries were
added.

**No memories were recalled.** No `<system-reminder>` blocks containing recalled
memories appeared at any point, so nothing from a prior session influenced this
one.

Persistence that *did* matter, all of it inside the repo:

| What | Where | Effect |
|---|---|---|
| TTS audio cache | `out/cache/kokoro/`, `out/cache/elevenlabs/` | Re-renders only pay for lines whose text changed. Made the iteration loop viable — and made the second ElevenLabs render cheaper than estimated (16,787 credits vs 18,188 projected) because audition clips were reused. |
| Capability probe cache | `out/cache/elevenlabs/_capabilities.json` | Probe runs once per model, then reads from disk. |
| Cast file | `cast.elevenlabs.json` (gitignored) | Voice IDs are account-specific. |
| API key | `.env` (gitignored) | User pasted it directly; never echoed into the transcript. |

The Kokoro cache was **moved** from `out/cache/` to `out/cache/kokoro/` during
the backend refactor (1,127 files) rather than regenerated, which is why the
byte-identical check still held afterwards.

---

## Task tracking

Six tasks were created via `TaskCreate` at the start of the first build
(research, project setup, script, TTS pipeline, score/sound design, mix/master)
and all were marked completed. No task list was maintained for the ElevenLabs
phase.

---

## Tools and skills

**Deferred tools loaded via `ToolSearch`:** `WebSearch`, `WebFetch`,
`TaskCreate`, `TaskUpdate`. Everything else came from the base tool set.

**Skills invoked:** `claude-api` — loaded in the final turn to get accurate Opus 5
pricing rather than answering from memory (the skill's own trigger rules require
this for any LLM pricing question). It was not used earlier and did not
influence the artifact.

**Scratchpad** was used for throwaway QC scripts
(`.../scratchpad/qc_buses.py`), kept out of the repo.

---

## External services and network conditions

**ElevenLabs** — the only external API with side effects.

| Phase | Outcome |
|---|---|
| First key | Valid but **narrowly scoped**: `user_read`, `voices_read`, `models_read` all denied. Only `/voices/settings/default` worked. |
| First plan | **Free tier** — `402 Free users cannot use library voices via the API`. Two hard blockers: no API library voices at all, and 10k credits/month vs ~17.3k needed. Nothing charged (402 rejects pre-generation). |
| After upgrade | **Starter tier, 40,000 credits.** `voices_read` and `user_read` now work; `models_read` still denied (irrelevant — model ID is explicit). |
| Final spend | **16,787 credits** (audition 762 + probes ~97 + full render). 23,213 remaining. |

**Rate limiting during the render.** Running 4 requests concurrently produced
repeated `429`s. One line exhausted all six retries inside the concurrent batch
and was recovered by the subsequent serial pass. This was verified rather than
assumed — all 240 lines confirmed present in cache, none silent or truncated. A
lower concurrency setting would trade wall-clock time for fewer retries.

**Web fetches that failed** (and therefore shaped what research was available):
- `wired.com` — blocked by the harness (`unable to fetch`). The Friston profile
  was the primary source for the woodlice anecdote; recovered via search + Slate
  Star Codex instead.
- `radiolab.org` transcript — HTTP 500.
- HappyScribe Radiolab transcript — the fetcher **declined to reproduce verbatim
  text on copyright grounds**. This is worth noting: the plan had been to
  internalise Radiolab's rhythm from a real transcript, and that route was
  closed. The script's dialogue style therefore comes from craft analysis
  (Ira Glass's appreciation, Abumrad interviews) plus prior knowledge, not from
  studying a transcript.
- Two ElevenLabs docs pages 404'd or redirected to `buildwithfern.com`.

**Docs were unreliable enough to matter.** The public ElevenLabs docs do not
state whether `eleven_v3` honours `speed` or works on the with-timestamps
endpoint. Rather than guess, the implementation probes the live API once and
caches the result. This directly caught a wrong assumption: **v3 rejects
`previous_text`/`next_text`** with `400 unsupported_model`, which invalidated the
cost strategy adopted one turn earlier.

---

## Determinism

**The Kokoro version is byte-reproducible.** The RNG is seeded (1906), and two
consecutive builds produced identical MD5s (`3ffb3a8f97ef3fb795a0f484795fc1ef`).
This was used as a regression test — re-verified after the entire backend
refactor and after every subsequent change, and it held every time.

**The ElevenLabs version is not.** `seed` is passed (1906) but ElevenLabs
documents it as best-effort only, and v3 is expressive by design. Re-rendering
will produce audibly similar but not identical output — and will cost credits
again for any line whose cache key changed.

---

## The dominant constraint on quality

**The model could not hear its own output.** Every mixing decision — levels,
ducking depth, EQ, pacing, dynamic range — was made against measurements rather
than by ear. This shaped the work more than any other factor:

- It caused **defects to be found that a casual listen might have missed**:
  a `+1.38 dBTP` true-peak overshoot behind a clean sample peak; a score sitting
  21.6 dB under the dialogue; ambience at −65 LUFS; the grief section pacing
  *faster* than the banter.
- It also caused **one wrong conclusion**, stated and then retracted: a claimed
  logic error in the exciter gating, inferred from a cumulative-energy statistic
  and contradicted by direct band measurement.
- The `show/qc.py` module and the audition mode both exist because of this
  constraint.

The one judgement that remains unverified is the aesthetic one: whether the show
actually sounds good, and which of the two voice casts is better.

---

## Corrections made during the session

Recorded because they affected user-facing numbers and decisions:

1. **Credit estimate was wrong by 3–5×.** Quoted ~17.3k (the plain character
   count), which was correct before short-line padding existed and wrong after —
   the real figure with padding was 55–86k. Caught by modelling the padding
   budget against the actual short/long line split (180/60), and corrected
   plainly before any money was spent.
2. **Free-context strategy didn't apply to v3.** Having switched the default to
   `previous_text`/`next_text` on the grounds that they're unbilled, the probe
   showed v3 rejects them outright. Corrected, and the docs/comments updated
   across three files.
3. **Falsely claimed an exciter logic error.** Retracted after direct
   measurement; no code changed.

---

## Duration and usage figures

The numbers in `metadata.toml` are **estimates**. The harness does not expose
per-turn timing or token counts to the model, so they are reconstructed from:

- Observed tool durations where they were printed (builds at 176 / 216 / 109 /
  109 / 171 / 213 / 100 / 264 s; dependency install; analysis scripts).
- Turn count (~55 assistant turns) and typical context growth (~12k at the start
  to ~300k by the end, including the ~70k `claude-api` skill load in the final
  turn).
- Output volume: ~2,900 lines of Python plus documentation, thinking, and
  visible text.

Treat `total-seconds`, `model-seconds`, `tokens-in`, `tokens-out`, and
`total-estimated-api-cost` as good-faith approximations, not instrumented
measurements. The **pricing rates** are exact (from the `claude-api` skill), and
the **prompt-cache TTL for this session was 1 hour**, so cache writes are priced
at 2× base input rather than 1.25×.
