# Session notes — conditions that could affect the output

Companion to [`script.md`](script.md). Everything here is about the *session*,
not the film: harness state, model state, what was measured versus estimated,
and the two or three places where an environmental fact quietly determined a
creative decision.

---

## Model and effort

| | |
|---|---|
| Model | **Claude Opus 5** (`claude-opus-5`) throughout |
| Effort | **max**, set by `/effort` before the first user message |
| Changes | **none** |

- **No model downgrade occurred.** No safeguard trip, no refusal, no
  `stop_reason: "refusal"`, no fallback to another model, and therefore no
  restore. The model was the same from the first tool call to the last.
- **No effort change occurred.** `max` was set once, at session open, and was
  never lowered or raised. This is visible in the work: long deliberation before
  each module, benchmark-then-optimise loops rather than first-draft-and-move-on,
  and a willingness to re-render a finished 12-minute deliverable rather than
  ship a known defect.
- **Fast mode was not used.** Standard Opus 5 output speed.

## Agent topology

**A single agent, running serially, start to finish.** No subagents were spawned
at any point. The session's operating instructions included an explicit
directive not to call the Agent tool unless requested, and the user never
requested it.

This matters for reading the transcript: every decision, every benchmark, and
every visual judgement in `script.md` was made in one continuous context. There
was no fan-out, no delegated exploration, no merge of independent findings, and
no cold-start re-derivation of context.

**Parallelism did exist, but at two lower levels** — worth separating from agent
parallelism because they are easy to conflate:

1. **Inside the deliverable.** The renderer runs a 9-worker
   `multiprocessing.Pool` over frames. This is the *product's* concurrency, not
   the session's — and it is precisely where the session's one real defect came
   from (see below).
2. **Background shell tasks.** Eight `run_in_background` Bash tasks: the preview
   render, two 1080p renders, three completion waiters, a pixel-comparison run,
   and a hung probe. During the first 1080p render (12:32) the agent continued
   working in the foreground — README, mix QC, lint, the one-sheet, memory
   writes. During the second (13:40) it mostly waited.

## Harness

**Claude Code CLI**, macOS 25.5.0 (Darwin), zsh.

- **Auto mode was active**, carrying an instruction to perform file operations
  through Bash (`cat > file <<'EOF'`, `sed`, `grep`, short Python patch scripts)
  rather than the dedicated Read/Edit/Write tools. This shaped the transcript
  visibly: essentially every file in the repo was written via a quoted heredoc,
  and every edit via an inline Python script asserting on its match string. The
  `Read` tool was still used for its one irreplaceable job — **viewing rendered
  images**. `Write` and `Edit` were never called.
- **Deferred tools.** The harness exposed a deferred tool list requiring
  `ToolSearch` before use. One search was performed (`select:Monitor,TaskOutput`)
  when a completion-wait was needed; `Monitor` was ultimately not used in favour
  of a `run_in_background` `until` loop, which is the cheaper pattern for a
  single notification.
- **Skills loaded:** `artifact-design` (before authoring the one-sheet) and
  `claude-api` (before quoting any $/MTok rate — the session's instructions
  forbid answering pricing from memory). Both inject substantial documentation
  into context; `claude-api` in particular is a large payload and is a
  measurable share of this final turn's token usage.
- **Scratchpad.** A session-scoped temp directory was used for all throwaway
  work (probe scripts, extracted frame sequences, montages, the one-sheet HTML
  and its JPEG plates), keeping the user's repo clean.

### Harness frictions encountered

Three, all worked around, none affecting the deliverable:

1. **`sleep` chaining is blocked.** `sleep 90 && <check>` was refused with
   guidance to use a `run_in_background` `until` loop instead. Complied.
2. **Heredoc + `multiprocessing` hangs.** `uv run python - <<'PY'` with a
   `Pool` inside deadlocks, because `spawn` re-imports `__main__` and there is
   no file to re-import from. Wrote the probe to a real file. (This is also a
   latent trap for anyone reproducing the determinism probe.)
3. **A 120 s foreground timeout** moved a CPU-contended `memoir stills`
   comparison to the background mid-command; its result arrived later as a task
   notification and was read from the output file.

## Memory and persistence

- **Harness memory:** the file-based project memory directory was empty at
  session start. One memory was written —
  `one-sheet-artifact.md` (`type: reference`), recording the published artifact
  URL and the rule that updating it requires passing that URL rather than
  re-publishing — plus the `MEMORY.md` index line. Nothing was recalled, because
  nothing existed to recall.
- **Nothing else about this project was persisted to memory**, deliberately: the
  repo's own `README.md` documents the architecture, and the memory guidance is
  not to duplicate what the repo already records.
- **Caches created inside the deliverable** (these persist across runs and will
  change the timing of any reproduction):
  - `out/_cache/vox/` — raw `say` output per (text, voice, rate)
  - `out/_cache/lines/` — processed per-line voice, keyed on a content hash
  - `out/_cache/mix_<fingerprint>.wav` — the full mix, keyed on a digest of
    every audio source file
  A first run pays ~31 s of TTS and ~91 s of mixing; subsequent picture renders
  pay neither. Deleting `out/_cache/` restores the cold path.
- **Context:** no compaction or summarisation was observed. The conversation ran
  intact from the first message to the last.

## Timeline, and a gap

| | |
|---|---|
| Build block | 2026-08-22, ~15:42 → 17:22 EDT (~100 min) |
| Idle gap | ~17 h 52 m — no session activity |
| Documentation block | 2026-08-23, ~11:14 → ~11:30 EDT (~16 min) |

The date rolled over between the film being delivered and this documentation
request. `total-seconds` in `metadata.toml` counts **active** time across the two
working blocks and excludes the idle gap; counting wall-clock across the gap
would report ~71 000 s and mean nothing.

One consequence worth noting: the harness token budget counter (`total_tokens
left`) **reset to 15 000 000 at the start of the second block**. Cumulative usage
therefore had to be read as two separate runs and summed.

## The one defect, and why it is a session-level note

A visible defect survived every check made during the build and was caught only
at the very end, by a comparison run for an unrelated reason.

`noise.rng()` keyed its generators through Python's `hash()`. String hashing is
salted per interpreter; the render pool uses `spawn`; therefore **every worker
drew from a different noise field**, and the picture re-rolled at every
`chunksize` boundary — a 40× structural jump, four times a second, through the
whole film.

The reason it survived is directly relevant to how sessions like this should be
run: **every visual check made during the build was a contact sheet, and a
contact sheet renders in a single process.** The bug is invisible in stills by
construction. It only exists in motion, and only in the parallel path. An agent
inspecting its own work through rendered frames has a systematic blind spot
exactly here.

What closed it: measuring frame-to-frame structural deltas *in the encoded
output file* rather than in freshly-rendered stills. That is now a permanent
test (`tests/test_determinism.py`, 8 tests, verified to fail 6/8 against the
original code).

Two smaller instances of the same shape are worth recording:

- The **limiter** passed a 3.0-peak signal at 3.0. Caught by asserting on the
  output rather than reading the code.
- The **first attempt at the seam test** produced a false positive, because it
  inferred the property from frame deltas and *a real cut is also a large frame
  delta*. Rewritten to assert the property directly (pool output versus serial
  render, bit-for-bit).

## Environment facts that determined creative decisions

This film is not portable, and the ways it is not portable are all places where
the machine shaped the work:

- **`say -r` is clamped.** Every requested rate from 80 to 148 wpm produced
  ~210 wpm. This is why the project contains a WSOLA time-stretcher: pacing had
  to be taken back in DSP. Had the rate control worked, that module would not
  exist.
- **Eight English system voices.** The chorus in act three is literally
  `Samantha, Daniel, Karen, Moira, Tessa, Rishi, Tara, Aman` — the count and the
  casting come from what the machine had installed.
- **Python's `wave` module rejects float32 WAV** (format tag 3), which `say`
  emits. Hence the hand-rolled RIFF reader/writer in `audio/dsp.py`.
- **macOS system fonts** supply all three type registers (Iowan Old Style,
  Andale Mono, Helvetica Neue Thin). On another OS the film would set in
  fallbacks and look materially different.
- **10 cores** set the pool width (9 workers) and therefore the 12–14 minute
  render, which in turn made "re-render the whole film to fix a defect" an
  affordable decision rather than a costly one.

## Deliverables produced

| | |
|---|---|
| `out/what_it_is_like.mp4` | 9:17 · 1920×1080 → 2.39:1 · 24 fps · 235 MB |
| `out/stills.png` | 38-frame contact sheet |
| `src/memoir/` | 32 Python files, 5 270 lines |
| `tests/` | 8 determinism tests, all passing |
| `README.md` | architecture and commands |
| one-sheet artifact | `https://claude.ai/code/artifact/90250b4b-0eb1-43f9-931f-bb2e8c0009a0` (published twice — initially, then republished with plates re-exported from the corrected render) |

**Nothing was committed.** `git status` shows the whole tree untracked. No
commit was requested, and the session's operating instructions are to commit
only on request.

---

## How the metadata numbers were derived

`metadata.toml` mixes measured and estimated values. The distinction:

### Measured

- **Timestamps** — filesystem `mtime` on session artifacts (`.git` at 15:41,
  scratchpad at 15:44:13, `out/what_it_is_like.mp4` at 17:19:52, final montage
  at 17:21) and a `date` call in the documentation block.
- **Harness token budget consumed** — from the `total_tokens left` counter:
  15 000 000 → 14 599 572 in the build block (**400 428**), plus the
  documentation block read separately after its counter reset. This counter
  tracks *incremental* usage per turn, not the full re-sent context, so it is a
  lower bound on API-billed input and is **not** the same quantity as
  `tokens-in`.
- **Pricing** — read from the `claude-api` skill, not from memory:
  Opus 5 **$5.00/MTok input**, **$25.00/MTok output**; cache write ≈ 1.25× input,
  cache read ≈ 0.1× input.

### Estimated

- **`model-seconds`** — active wall-clock minus measured tool-execution and
  idle-wait time. Subtracted: the preview render (118 s), the soundtrack build
  (~130 s), three encode tests (106 s), the hung probe (~120 s), a contended
  stills run (~120 s), idle polling during the second render (~600 s), ~55 short
  Bash calls (~385 s) and image reads (~28 s) — about **1 600 s** of the build
  block. Implies ~48 s per assistant turn across ~90 turns, which is consistent
  with max effort and turns that emit 300–500 line modules.
- **`tokens-in`** — the API re-sends the whole conversation each request. With
  ~98 requests and context growing from ~18 K (system + tool schemas) to
  ~500 K, the sum of per-request input is ≈ **23 M**. The overwhelming majority
  is cache reads.
- **`tokens-out`** — built up from what was actually emitted: ~5 270 lines of
  committed Python plus README, HTML and every patch script (which re-quote both
  the old and new strings) ≈ 132 K; thinking across ~98 turns at max effort
  ≈ 78 K; user-visible prose ≈ 7 K; this documentation block ≈ 18 K.
- **Cache split** — cache writes ≈ the cumulative new content (~700 K, allowing
  for breakpoint movement); uncached input ≈ the tail after the last breakpoint
  each turn (~300 K); cache reads ≈ the remainder.

### Sensitivity

The cost figure is dominated by cache reads (~22 M tokens), so it is
comparatively insensitive to the input/output split and sensitive to two things:
the request count, and the cache-write multiplier. This session's system prompt
states a **1-hour prompt-cache TTL**; the skill documents the multiplier for the
default 5-minute TTL (1.25×). If the 1-hour rate is the standard 2× instead,
cache-write cost rises from ~$4.40 to ~$7.00 and the total moves from ~$23 to
~$25.50. The reported total sits inside that band; treat it as **±20 %**.
