Skip to content
C.W.K.
Lesson 05 of 05 · published

Three Slots, One Family Value Each, and Turns That Survive

~14 min · brains, settings, concurrency, recovery

Level 0Mise en Place
0 XP0/43 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

One brain per function

피파 브레인은 기능별로 설정 가능해야해. firelink에도 등록하고. 글로벌 설정이니까.

Dad, on the design day: Pippa's brain has to be settable per function, registered in Firelink too, because it is a global setting.

That is ruling 9, and it gave Sizzle three slots in the family Brain Control Plane. Each slot is one function, and its value is one global family row. Sizzle's Settings page and Firelink's Brains tab edit the same row.

SlotLabelCategoryWhat runs on it
readingPlate readingutilityevery capture, correction and re-read; needs a brain that can see images (Track 3)
talkPlate and class talkmainconversations about one plate or one class, with the changes I propose
kitchenKitchenmainthe kitchen conversation

slot_for_thread is two lines: the kitchen key goes to the kitchen slot, every other thread to talk. Pippa Quest's track on Pippa's bodies, the multi-brain variants, teaches the brains themselves. What matters here is that Sizzle never picks one at run time. The only brain names in its engine code are first-registration seeds, like the two in the block below, and no path falls back to a brain when resolution fails. Its house sign says it in four words: never hardcode a brain.

Declared every start, seeded once

At startup the engine declares its three slots before any job can resolve one, so a re-read that starts at boot never races its own registration. The family authority sits behind Firelink's loopback facade. If it does not answer, declaration only waits, because resolve declares lazily on first use. Each new process declares again, carrying the same values, and those values seed a slot's first registration and nothing more. A slot central already holds keeps its value and gets only its label and description refreshed. Reading starts as Claude at high effort, the engine's configuration from before the plane existed. Talk and kitchen start as Claude on the brain's own default effort. The docstring says it plainly: central values are never overwritten from here. An engine test pins all of it: the three slot ids, the reading slot's explicit high level, the two conversations on inherit, and the labels in order.

A second test runs each function on its own slot. It seeds reading as Kimi, talk as Claude and kitchen as Codex at medium, then checks what each call actually received. The kitchen turn arrives as chatgpt, because the family's central name for that brain and the picker's name differ, and the kit translates between the two.

Resolved at turn start, never guessed

Threads._run resolves the thread's slot at the start of every turn, inside that thread's lock. A brain Dad saves takes effect on the next turn of that function, in the middle of a conversation if need be. That works because cwkPippa builds every turn from stored history, the property Master Chief Quest calls any brain, same conversation.

The kit is honest about outages. It asks the central authority first. If that fails, it serves the last known good copy marked stale, and Settings shows a line saying so. With no copy at all, or with an assignment central itself marks invalid, it raises BrainControlUnavailable, and the turn is recorded as failed with No brain assignment for this conversation and the reason. A test makes the authority unreachable from the very first start and confirms that no reading runs. There is no hidden default to fall back on.

One row, two editors

Two screens that edit one value need a rule for collisions. Settings sends the slot's brain, its effort or inherit, and the revision it rendered. If Firelink or another device changed the row in between, the write comes back as a 409 conflict, and the page reloads with It changed elsewhere just now. The current value is reloaded; choose again. Dad decides again with the real value in front of him. Firelink Quest's track on one owner per fact is the same idea at family scale. Saving a new reading brain wakes the reading worker, so plates still waiting use it at once. For a brain whose effort levels belong to each model, as Ollama's do, Sizzle holds no list of levels and leaves the check to the central owner, which answers with a visible 400. A brain with a static list is still held to that list.

Turns that survive a restart

A turn is recorded before it runs. ask writes turn.asked to the journal and only then starts the task. A second ask with the same request id returns the existing turn, so a client that lost a reply and retries never asks twice. A kitchen test sends one body twice and counts one call. One lock per thread keeps cwkPippa's history in order.

When the engine restarts, some turns may still be pending. resume runs at startup, and it never resends. For each pending turn it asks cwkPippa for the stored reply bound to that turn's request id, with exact=True. A match is recorded as the answer, and its actions are validated like any others. No match marks the turn failed: Interrupted by an engine restart. Ask again. The live path is looser. Right after its own stream ends, a turn lets the newest assistant reply stand in when cwkPippa did not echo the request id, and at that moment the newest reply is the one it just received. After a restart, the newest reply could belong to an earlier turn, so only an exact match counts.

Ask again is a new question. The button sends the same words as a fresh ask with a new request id, and the failed turn stays in the record as it was. Only a send whose HTTP reply was lost keeps its old id, and that is the retry the engine answers with the existing turn.

Principle: A turn that might have happened is looked up, never repeated. Record the intent first, bind it to an id the other side stores, and after a crash ask for that id exactly.

Code

cwkSizzle, backend/brain_control.py: first-registration seeds, one declaration, resolution at job start (verbatim, lines 92-117)·python
def _initials() -> dict[str, dict[str, str]]:
    """First-registration values only. Reading keeps the engine's pre-plane
    configuration; the two conversations follow the brain's own default
    effort. Central values are never overwritten from here."""
    return {
        READING: _bound.initial(settings.READING_BRAIN_INITIAL, settings.READING_EFFORT_INITIAL),
        TALK: _bound.initial("claude"),
        KITCHEN: _bound.initial("claude"),
    }


def adopt_once(_brain: str | None = None, _effort: str | None = None) -> bool:
    """Declare the three slots once per process. Sizzle never had a device
    picker, so the browser's adoption parameters are ignored."""
    return _bound.adopt_once(build=_initials)


def resolve(slot_id: str) -> dict:
    """Job-start resolution: picker-namespace brain, concrete effort, model
    pin. Raises ``BrainControlUnavailable`` fail-closed."""
    if not _bound.adopted:
        try:
            adopt_once()
        except Exception as exc:  # noqa: BLE001 - resolve below reports the outage honestly
            logger.warning("brain-control: declaration deferred: %s", exc)
    return _bound.resolve_slot(slot_id)
cwkSizzle, backend/threads.py: after a restart, recover by exact request id and never resend (verbatim, lines 96-108)·python
    async def resume(self) -> None:
        """After a restart: recover answers cwkPippa already has, never resend."""
        for turn_id in self.sizzle.store.unfinished_turns():
            turn = self.sizzle.store.turn(turn_id)
            conversation_id = self.sizzle.store.conversation_for(turn["thread_key"])
            found = None
            if conversation_id:
                found = await pippa.last_answer(conversation_id, turn["request_id"], exact=True)
            if found:
                self._record_answer(turn, found["content"], found["emotion"])
            else:
                self.sizzle.record("turn.failed", {"id": turn_id,
                                                   "error": "Interrupted by an engine restart. Ask again."})
cwkSizzle, frontend/src/pages/Settings.tsx: a save carries the revision it rendered, and a conflict reloads (verbatim, lines 82-102)·typescript
  const save = async () => {
    setSaving(true);
    try {
      await brainApi.update(info.slot_id, {
        brain,
        effort: effort || null,
        inherit: !effort,
        expected_revision: current?.revision ?? null,
      });
      setError(null);
      onSaved();
    } catch (exc) {
      const message = (exc as Error).message;
      // A 409 means the value moved elsewhere (Firelink, another device)
      // after this page rendered it: reload and let Dad decide again.
      setError(message.includes("conflict") ? "It changed elsewhere just now. The current value is reloaded; choose again." : message);
      onSaved();
    } finally {
      setSaving(false);
    }
  };

External links

Exercise

Give two functions of an assistant in your app their own model settings, kept in one shared place that two screens can edit. Seed each setting once at first registration, resolve it at the start of every job with no hardcoded fallback, and make every write carry the revision it read so a stale screen gets a conflict. Then kill the process in the middle of a request, restart it, and make startup either recover the finished answer by request id or mark the request failed, without ever sending it again.
Hint
Store the request id on both sides before the call leaves. Recovery needs an exact lookup by that id. Taking the latest answer is safe only while your process is still the one holding the stream.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.