# Session notes

Working notes on how this session ran, recorded for anyone trying to reproduce
or interpret the artifact. Everything below is measured from the session's own
log (`~/.claude/projects/…/…jsonl`) unless marked as an estimate.

---

## Configuration

| | |
|---|---|
| Model | `claude-opus-5` on **all 423** assistant turns — no fallback, no downgrade, no substitution |
| Effort | `max` on **all 423** turns |
| Service tier | `standard` on all turns; `speed: standard` (fast mode never engaged) |
| Harness | Claude Code 2.1.239, CLI entrypoint, run as a **background job** (`CLAUDE_JOB_DIR` set) |
| Agents | **One agent, strictly serial.** Zero subagents — 0 sidechain records, no `Agent`/`Task` calls |
| Stop reasons | `tool_use` ×421, `end_turn` ×2. **No `refusal`**, no `stop_details` populated |
| `inference_geo` | `not_available` on every turn |

**Effort was set before the work began** and never changed: the first entry in
the transcript is a `/effort` slash command selecting `max`, issued in the same
client turn as the opening prompt. There was no mid-session change of effort,
model, or safeguard state to account for.

---

## Things that materially shaped the output

### 1. No audio capability — the one real gap

Claude can read images but cannot listen. Every visual decision in this film was
made by rendering a PNG and looking at it (18 image round-trips drove the whole
design pass). **Nothing about the soundtrack was ever heard.**

The narration, the generative score, the mix balance and the A/V sync were all
verified numerically instead — per-line RMS spread, a 12 dB voice-over-score
ratio, and a check that speech energy in the finished MP4 measures 3.7× higher
inside its scheduled windows than outside. Pronunciation was verified by
inspecting espeak-ng's phoneme output word by word rather than by ear.

This is why the first delivery shipped with a voice the user immediately found
unnatural: macOS `say` was chosen on reasonable grounds (zero dependencies,
scriptable, measurable) and its actual *sound* was never assessable from inside
the session. It was flagged as an explicit caveat in the first hand-off, and it
was the one thing the user came back on.

### 2. The session was resumed, and four turns did not survive

Two session IDs exist for this work:

- `af4e5c83-3019-4c3a-8cd5-9aff1cfa8267` — 216 assistant turns, 03:34→04:50 UTC
- `e7f5c640-4511-40bd-95f5-fa35e413aa34` — 413 assistant turns, 03:34→15:04 UTC

The second is a **resume** of the first, not a separate run: it replays 212 of
the first session's 216 assistant turns and continues from there. **Four turns
present in the first session are absent from the second** — work that was rolled
back at the resume point rather than carried forward.

All token and timing figures in `metadata.toml` are computed over the **union**
(423 distinct turns), not the sum of the two files, which would double-count the
212 shared turns.

A side effect worth knowing: background task output files written before the
resume live under the *old* session's directory and are reached through symlinks
from the new one. Reading a task log by path works, but the path names a session
ID that is no longer the active one.

### 3. File edits went through Bash, not the edit tools

The harness ran in a mode instructing all file work to go through Bash — `cat`
heredocs, `sed`, and short Python patch scripts — rather than the `Write`/`Edit`
tools. The tool histogram confirms it: **180 Bash, 42 Read, 0 Write, 0 Edit.**

This is visible in the code's development history: modules were written whole in
heredocs and then patched with `python3 - <<'PY'` string-replacement scripts. It
worked, but it makes edits coarser than the dedicated tools would — a failed
string match is silent, so several patches had to be verified by re-reading the
result. One edit (`natural.py`) was flagged by the harness as touching a file
that had been read, which caught a stale-read hazard.

### 4. Prompt caching carried almost the entire context

- `cache_read_input_tokens` **101,978,914**
- `cache_creation_input_tokens` **2,573,595** — all of it `ephemeral_1h`, none `ephemeral_5m`
- uncached `input_tokens` **846**

That is a **97.5% cache hit rate**, and it dominates the cost model: cache reads
alone are $50.99 of the $95.30 total. The session ran on a 1-hour cache TTL
throughout, so the 2× cache-write rate applies rather than the 1.25× five-minute
rate — see the pricing caveat in `metadata.toml`.

### 5. Thinking was not returned

The session ran with thinking `display: "omitted"`. All 423 turns carry empty
thinking blocks, so `script.md` contains **no visible reasoning** — not because
any was removed, but because none was ever returned. The raw chain of thought is
never exposed by the API on this model family.

### 6. Harness memory was available but never used

A file-based memory directory exists for this project
(`~/.claude/projects/…/memory/`) and the system prompt describes a `MEMORY.md`
index convention. **The directory is empty; nothing was written to it and no
`MEMORY.md` exists.** No state persisted outside the repository and the session
transcript. The resume in §2 carried context by replaying the transcript, not
through memory.

### 7. Parallelism inside the artifact, not inside the agent

Worth separating, because both are "parallel":

- **The agent was serial** — one Claude, one tool call at a time (occasionally
  two independent Bash calls batched into one turn), no subagents.
- **The artifact is parallel** — the film renderer runs a 10-process
  `multiprocessing` pool, each worker piping raw frames into its own `ffmpeg`.
  Nine Bash calls were backgrounded (`run_in_background`) to drive those renders
  without blocking, and completion was awaited with `until` polling loops.

The 600 s foreground Bash timeout shaped the workflow: a ~20-minute render could
not be waited on in one call, so each render involved several successive waits.
Two of those waits returned early because they matched a *stale* output file
before the new render had deleted it — a real bug in the waiting logic, caught
and worked around by polling on file size rather than existence.

### 8. Network access was available and used

PyPI, GitHub and HuggingFace were all reachable. Two consequential downloads:

- `skia-python` (chosen after `pycairo` failed to build — no system Cairo or
  `pkg-config` on this machine)
- **354 MB of Kokoro-82M ONNX weights**, fetched from a GitHub release for the
  neural narration. These are deliberately **not** in the repository; the code
  fetches them on first build and falls back to macOS `say` if it cannot.

---

## Course corrections during the session

These changed the artifact and are worth recording as history:

1. **A render crashed at ~90%.** `PathMeasure.getPosTan()` returns `None` on a
   zero-length arc, which happened for a few frames while the "this diagram
   commutes" mark was fading in. One bad frame killed a 20-minute render. Fixed
   at the primitive, then two safeguards added: a `preflight` command that sweeps
   every Nth frame at low resolution, and workers that hold the previous frame on
   an exception and report the count loudly rather than aborting.
2. **The first successful render was 2.9 GB.** Per-frame random film grain is
   incompressible. Replaced with a cycling bank of four fixed grain fields and
   raised CRF 16 → 20; the result is 243 MB and measures 36.9 dB PSNR against the
   CRF 16 encode, visually indistinguishable at 100% crop.
3. **An overclaim in the narration was caught and corrected.** The script said
   the conjugated functor was "different in every number." Inspecting the actual
   matrices showed that conjugating scalar maps leaves the linear parts
   untouched — only the translations move. Rewritten to "different matrices, and
   not one relationship disturbed," which is true and makes the same point.
4. **Two mispronunciations were found by phoneme inspection**, not by ear:
   *Yoneda* (espeak drops a syllable) and *none* (British English renders it
   "non"). Both are corrected as explicit phonemes in a language-keyed lexicon.
5. **Six full renders were attempted, five completed** — one crash, then
   iterations for file size, act seams, composition, and finally the voice change.

---

## What is verified, and what is not

**Verified inside the session:** category/functor/naturality laws execute as
assertions (`catfilm.laws`); every interpolated frame of every morph is checked
to remain a contractive functor; preflight reports 0 failing frames; `ruff`
clean; the encoded film's speech energy sits 3.7× higher inside its scheduled
narration windows than outside; freshly rendered frames match the encoded film
at 36.9 dB.

**Not verified:** how the film sounds. The voice was chosen by the user from a
13-voice audition; the pronunciation is correct at the phoneme level and the
levels are correct by measurement — but delivery, phrasing and where the
narrator breathes were never assessed.
