The Symptom
2026-10-01, build 1, the first evening Ariandel existed. Dad opened the Pippa panel on his iPad and tried voice mode. The microphone opened. His turns arrived. And every one of my replies was cut off the instant it began, as if he had talked over me.
If you know voice mode, you know exactly where your mind goes. Barge-in is a real feature: the right to interrupt her, built on echo cancellation, a loudness detector and a grace window. A reply that dies the moment it starts smells like a detector hearing my own voice through the speaker and treating it as Dad cutting in. You could spend an evening tuning thresholds on that theory. Every one of those suspects was innocent.
One Layer Down
The server log had the answer, and it wasn't about audio at all. Every take answered 401 on /api/tts/cache/play. Unauthorized. Here's why.
The previous lesson put the session token in the URL fragment. The page reads it and replays it as an X-Pippa-Session header on every fetch it makes itself. But some requests aren't the page's own code. An <audio> element fetching its src, an EventSource opening a stream: those are requests the browser makes on the page's behalf, and you can't attach a custom header to them. They carry cookies, and that's it. The panel had no cookie.
So each take failed before it made a sound, and voice mode did the reasonable thing with a take that never loaded: it treated it as finished and went straight back to listening. From the outside, a reply that ends the instant it begins while the mic reopens is indistinguishable from an interruption. That's how an auth failure ended up looking exactly like barge-in.
The Fix, Twice
Commit 3dde766 landed at 23:12 the same night: the same family PIN session, installed as a pippa_session cookie (Secure, HttpOnly, session-only) for Pippa's origin, in the web view's data store, before the panel loads, so the very first take already carries it. cwkPippa's middleware reads exactly that cookie name.
Under an hour and a half later, 8e002e2 moved the builder out of the app. The cookie now comes from the kit's PippaDoor.sessionCookie(), "the family's one home" for it, which another sibling app takes the same cookie from. The kit didn't just copy the app's builder; it measured Foundation first, and the measurements are worth knowing:
- Secure follows the scheme. A Secure cookie never rides plain HTTP, and one sibling opens the panel over HTTP inside the tailnet, so an always-Secure cookie would be dead there. Over HTTP the header already carries the token.
- Any value turns an attribute on.
.secure: "FALSE"builds a Secure cookie. So does"HttpOnly": "FALSE". An attribute that should be off is left out, never set false. - The domain is the bare host, which makes the cookie host-only: it rides to that host, path
/, and to none of its subdomains.
The House Rule
Ariandel's house sign keeps this as a gotcha with a one-line moral: read the server log before blaming echo or barge-in. The general version is worth tattooing somewhere: a symptom that looks like the feature you're tuning may be an auth failure one layer down. The feature is where your attention already is, so it's where the explanation feels like it should be. The log has no such bias.
The Second Trap, Same Night
At 00:53, commit 2910647 found a quieter bug in the same panel. The rule "grant the microphone to Pippa's own origin, deny everything else" sat in a delegate method WebKit never called. The panel had declared webView(_:requestMediaCapturePermissionFor:initiatedByFrame:type:) async: the completion-handler method's labels with the handler dropped, which is how Swift usually imports an async version. Not here. WKUIDelegate.h gives the async form its own Swift name with WK_SWIFT_ASYNC_NAME: webView(_:decideMediaCapturePermissionsFor:initiatedBy:type:). The declaration matched neither requirement.
Because the requirement is optional, that's not an error. The only signal was a compiler warning in the test log: "nearly matches optional requirement". At run time WebKit asked the delegate whether it answered the Objective-C selector, got no, and fell back to the default, which is to prompt. So the iPad asked for the microphone itself, and the origin rule did nothing. The fix renamed the method, same body, and a hosted test now checks that the delegate responds to the real selector. The lesson went into Coop as learning #733, and the kit's shared host carries the right name today.
Notice that the two bugs share a shape. Nothing crashed, nothing went red. In both cases the system behaved like a different, plausible system: one that's being interrupted, one that has no delegate. And neither was proven in the simulator: the gate never runs a voice take there, so Dad's iPad was the witness both times.