Fix caller lineup never loading and add Wellspring cult callers

The lineup fix and the new characters both land in main.py, so they share a
commit rather than being split artificially.

Lineup fix — populate_backgrounds() was reachable only through
POST /api/session/reset, so taking calls without hitting reset left
session.caller_backgrounds empty for a whole show. Every caller got a hollow
prompt, collapsed into generic relationship filler, and Silas lost his lore.
Now start_call populates on demand, get_caller_prompt raises
EmptyCallerBackgroundError instead of emitting an empty prompt, /api/callers
reports lineup readiness to a header badge, and cross-episode topic dedup
widened from 2 shows to 10.

Wellspring callers — Doyle as the anchor defector plus a six-member pool,
grouped by new frontmatter fields (group, group_lead, group_weight, register,
explicitness). A faction is capped at one caller per show, with a 15% roll for
two pairing the lead with one other member. group_weight lets an anchor carrying
an arc get slots faster than texture characters.

Also detects model refusals, which arrive as ordinary 200s and previously
reached air as broken-character text or dead silence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 03:33:08 -05:00
co-authored by Claude Opus 5
parent 9071ed36d3
commit b85e1c6ea7
15 changed files with 856 additions and 21 deletions
@@ -0,0 +1,172 @@
# The Wellspring Callers — Design
**Date:** 2026-08-15
**Status:** Approved, not yet implemented
## Problem
Silas is the show's strongest recurring character, but he is the only window into
The Wellspring — and he controls the framing entirely. He describes a warm
commune of seekers. Nobody contradicts him. The bit is funny but static, and the
cult has no weight because there are no stakes the audience can see.
## Goal
Add callers from inside The Wellspring who offer perspectives Silas does not
control — including frightened ones — so the commune becomes a place with real
consequences rather than a running gag. Keep Silas playable as comedy.
## Structure
An **anchor** character with a progressing arc, plus a **pool** of rotating
members for fresh angles.
- The anchor carries continuity and escalating dread.
- The pool supplies variety and prevents the counter-perspective from
flattening into one note.
- Silas remains the leader's version of events.
## The anchor: Doyle
```yaml
name: Doyle
voice: Grant # plain, tight — deliberate contrast with Silas's Sebastian
age: 47
group: wellspring
register: dry, understated, factual
explicitness: low
```
Two years in. Joined eight months after his divorce, when a member sold him soap
at a farmers market in Las Cruces and asked him a question nobody had asked him
in a year. He runs the soap kettles — twelve-hour days, physical, unglamorous.
The soap room is the engine of the character: **it is where the money is, and
nobody spiritual pays attention to it.** He sees shipping manifests, deposit
slips, whose names are on which accounts. He is not a seeker. He is the guy who
fixes the pump.
**His fear is specific.** He is not afraid of being hurt. He is afraid of being
right. He has a truck, a CDL, and nowhere to be — he could leave tomorrow, and
has been telling himself that for eleven months. What frightens him is that he
keeps not leaving and can no longer tell whether that is his choice.
**Calling mechanic.** No signal at the Terlingua property. Doyle calls from town
on supply runs — a gas station lot in Study Butte, engine running, twenty
minutes before someone notices the truck has been gone too long. This caps call
length naturally, explains why he cannot talk long, and gives the host a ticking
clock. An abrupt hangup is in character, not a failure.
**Register.** He describes appalling things in the flat voice of a man reading a
parts list. The comedy and the dread come from the same place: he does not
editorialize.
## The pool
Six members, each an ordinary regular file, drawn one at a time.
| Member | Voice | Age | Register | Explicitness |
|---|---|---|---|---|
| Renata — true believer | Celeste | 34 | Radiantly happy, evangelical, funny | high |
| Alvin — bookkeeper | Conrad | 61 | Dry, precise, funny-then-cold | low |
| Junie — grew up there | Pippa | 19 | Guileless, no frame of reference | low |
| Ford — fourteen days in | Levi | 28 | Love-bombed, evangelical, too fast | medium |
| Marguerite — wants out | Marlene | 58 | Exhausted, practical, sad | medium |
| Cyrus — inner circle | Damon | 41 | Charming, then not | medium |
**Junie is a fixed constraint.** Someone who grew up inside the commune is the
show's darkest available note, and she works precisely because nothing explicit
ever attaches to her. Her horror is that she believes all of it is normal. Her
`explicitness: low` is not a default — it does not change.
## Tone
Mostly unsettling-but-comic, with the capacity to go genuinely dark. The
Wellspring should carry real weight without every call being heavy. Register is
assigned per character rather than globally, so the true believer plays funny in
the same season Doyle plays frightening.
**Adult content.** The show is adult and the Wellspring's sexuality is already
load-bearing in canon. Characters may talk frankly about sex and kink. This is
controlled per character via `explicitness: low | medium | high` rather than a
global switch, so explicitness varies by who called and the show does not
flatten into wall-to-wall smut.
**The one hard separation: consensual weirdness and coercion stay in different
lanes.** An openly horny commune with elaborate rituals is the comedy. Silas
pressuring a member into participating is the horror — that is where the weight
comes from. Blurring them casually makes the show mean rather than dark and
makes Silas irredeemable, which costs the character. Existing canon already
handles this correctly: his 2026-03-15 reckoning treats the coercion as real
harm.
## Selection mechanics
Current behavior (`backend/main.py`, `_build_backgrounds`): every active regular
draws independently at `min(1.0, 0.4 + 0.3 × shows_missed)`, then the list is
truncated to 3.
Adding eight Wellspring files to that would let the commune eat every regular
slot. So:
1. Partition regulars by the new `group` frontmatter field.
2. Ungrouped regulars behave exactly as today.
3. Within `group: wellspring`, members draw on their existing per-member
probability, then the group is **capped**:
- **1** Wellspring caller per episode normally
- **2** on a 15% roll — a "both sides tonight" episode
- On a two-night, prefer Silas plus one non-Silas member
4. Group cap applies before the global 3-regular truncation.
## Data flow
```
~/code/dotfiles/regulars/*.md (+ silas/silas.md)
↓ regulars_v2.load_all_active_regulars()
↓ Regular gains: group, register, explicitness
↓ _build_backgrounds() — group partition + cap
↓ regulars_included
↓ caller_gen.build_batch_prompt() — renders register + explicitness
↓ Sonnet 4.6 batch
↓ session.caller_backgrounds
↓ get_caller_prompt() — live dialog
```
Touch points:
- `regulars_v2.py:29` `load_regular()` — parse four new frontmatter fields
(`group`, `group_lead`, `register`, `explicitness`), defaulting so existing
files keep working
- `regulars_v2.py:19` `Regular` dataclass — four new fields
- `main.py` `_build_backgrounds()` — group partition and cap
- `caller_gen.py:132-138` — render `register` and `explicitness` into the batch
prompt (currently renders only name, lore, arc_state)
- Eight new markdown files; one frontmatter line added to `silas.md`
## Failure mode to guard
Model refusals do not look like errors. `llm.py:385-395` handles exceptions and
empty completions by returning `None` with a printed message, but a content
refusal typically arrives as a **200 with refusal text**, which flows through as
ordinary dialog and breaks character on air — or as an empty completion that
reads as dead air.
Add explicit detection so a refusal is visibly distinct from a caller who simply
had nothing to say, surfaced in the host's log rather than only stdout. Without
this, tuning `explicitness: high` upward has no feedback signal.
## Testing
- `load_regular()` parses the new fields; files lacking them still load
- Group cap holds: 8 Wellspring regulars never yield more than 2 in a lineup
- Two-night rate approximates 15% over many seeded draws
- On a two-night, Silas is preferred plus exactly one non-Silas member
- Ungrouped regulars are unaffected by group logic
- Batch prompt contains `register` and `explicitness` for each included regular
- Junie's `explicitness` is `low` (guards against a careless edit)
## Out of scope
- Arc progression automation — arc_state stays hand-edited, as today
- Promotion of pool members to anchors
- Any change to how walk-in callers are generated