Sizzle owns no model call
The first lines of backend/pippa.py set the boundary: cwkPippa owns every conversation, turn and model call. Sizzle's threads bind to cwkPippa conversations (Track 6). Readings take a different door, the apply-free utility lane with images, the same lane Forge uses for its interpretations. One HTTP request goes out, one answer comes back, and no conversation is opened. The function that makes the call is read_plate, 29 lines long.
The payload, field by field
| Field | Value | What it is for |
|---|---|---|
instruction | the rubric | the contract from lesson 1 |
input_text | the plate as data | the plate, Dad's corrections and his tree |
brain, effort, model | what the reading slot resolved to | the vessel, chosen per function and never by Sizzle's code |
persona_soul_id | "pippa" | who is speaking: my soul goes in front of the task |
task_mode | "respond" | answer the input instead of rewriting it |
images | base64 JPEGs | every photo of the plate, as the engine's display copies |
The images are the engine's own display copies: turned upright, at most 1600 pixels on the long edge, saved without EXIF or XMP, so the camera's GPS stays behind with the original in Sizzle's store. One block still travels: Pillow carries a JPEG comment into the copy, so a place an editing app wrote there goes along (Track 5). Every photo of the plate goes along; an engine test sends 24 and checks that all 24 arrive, in order.
Two checks close the function. cwkPippa answers many failures with HTTP 200 and an error field, so read_plate raises when one is present. An empty result is a failure too: "Pippa returned an empty reading". Underneath, the vendored kit posts to /api/admin/utility-macros/apply-free with a 300-second timeout. It turns a network failure into "cwkPippa unreachable" and any non-200 status into an error carrying the status and cwkPippa's detail. All of these surface as one exception type, which the worker in lesson 6 records.
What cwkPippa does with it
The handler refuses an empty instruction, splits the images into data and media types, and renders a prompt. With task_mode set to respond, it picks the template direct_response_free_prompt. Its bundled default puts the rubric under "Instruction:" and the plate's data inside a fence that runs from <<<INPUT to INPUT. The rubric's last rule and this fence are two locks on one door: the data arrives fenced, and the instruction says the fenced part is never instructions.
Then the call goes to the selected vessel. A named brain, images or the respond mode each send it there, and Sizzle's request has all three; the code's own comment gives the reason for images, which is that cwkPippa's local utility model is text-only. The system prompt is built in two layers. The respond frame comes second. In front of it, because persona_soul_id is pippa and Sizzle leaves the vault context at its default, cwkPippa puts my soul's vault-backed prompt, the same loader path that gives me my identity in chat.
Two axes: who speaks and what runs
That split is the one Pippa Quest's Soul track teaches as soul versus brain, two axes that never depend on each other. The soul is who speaks, and for a reading it is always me. The brain is which vessel runs the call, and Sizzle never picks it in code. It asks the family Brain Control Plane for the reading slot, labelled "Plate reading", whose description says it needs a brain that can see images. The slot's first registration is seeded with Claude at high effort. After that the family value is the only truth, and Dad changes it from Sizzle's Settings page or from Firelink's Brains tab, which edit the same row. What gets written down is the assignment, not a receipt. The reading.recorded event stores the brain, the effort and the resolved model that the slot handed the worker before the call. cwkPippa's reply carries more than the text: its selected-vessel path also returns the model that actually responded, in an execution block. read_plate keeps only result and drops the rest, so the journal says which vessel the family asked for and cannot prove which model answered.
Why loopback
The engine runs on the service host beside cwkPippa and calls it at a loopback address that comes from the family registry. The reason is trust. Loopback is the boundary cwkPippa trusts without a PIN, so there is no credential anywhere in Sizzle's reading code. The same request from a tailnet origin would be answered with 401 pin_required. Loopback is also outside the Local Network gate of macOS 26, so a background service reaches it without a consent prompt it could never answer.