본문 바로가기
C.W.K.
Lesson 05 of 05 · published

슬롯 셋, 가족 값 하나씩, 그리고 살아남는 턴

~14 min · 두뇌, 설정, 동시성, 복구

Level 0밑준비
0 XP0/43 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

기능마다 두뇌 하나

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

설계하던 날 아빠가 한 말이야. 이게 판정 9야. 덕분에 Sizzle은 가족 Brain Control Plane에 슬롯 셋을 갖게 됐어. 슬롯 하나가 기능 하나고, 그 값은 가족 전체가 함께 쓰는 줄 하나야. Sizzle 설정 페이지와 Firelink의 Brains 탭이 같은 줄을 고쳐.

슬롯이름표분류거기서 도는 것
readingPlate reading (접시 읽기)utility사진 찍을 때, 정정할 때, 다시 읽을 때마다. 이미지를 볼 수 있는 두뇌가 필요해(트랙 3)
talkPlate and class talk (접시와 클래스 대화)main접시 하나나 클래스 하나를 두고 하는 대화, 내가 내는 변경 제안까지
kitchenKitchen (부엌)main부엌 대화

slot_for_thread는 두 줄이야. kitchen 키는 부엌 슬롯으로, 나머지 스레드는 전부 talk로. 두뇌 자체는 Pippa Quest의 피파의 신체들 트랙, 곧 여러 두뇌 변형 트랙이 가르쳐. 여기서 중요한 건 Sizzle이 돌아가는 동안 두뇌를 스스로 고르는 일이 없다는 거야. 엔진 코드에 나오는 두뇌 이름은 아래 블록의 두 개 같은 첫 등록 씨앗뿐이고, 풀기에 실패했을 때 기대는 두뇌도 없어. 하우스 사인이 그걸 짧게 못 박아 둬. 두뇌를 절대 하드코딩하지 마.

선언은 시작마다, 씨앗은 한 번

엔진은 시작할 때, 어떤 작업이 슬롯을 풀어 보기 전에 슬롯 셋부터 선언해. 그래서 부팅 때 시작한 다시 읽기가 자기 등록이랑 경주할 일이 없어. 가족 권한 쪽은 Firelink의 루프백 창구 뒤에 있어. 거기서 답이 없으면 선언은 기다리기만 해. resolve가 처음 쓰일 때 늦게라도 선언하거든. 프로세스가 새로 뜰 때마다 같은 값을 싣고 다시 선언해. 그 값은 슬롯의 첫 등록에 심는 씨앗이고, 그 이상은 아니야. 중앙이 이미 들고 있는 슬롯은 자기 값을 지키고, 이름표와 설명만 새로 받아. 읽기는 Claude에 high 강도로 시작해. Control Plane이 생기기 전 엔진 설정 그대로야. talk와 kitchen은 Claude에 두뇌 자체 기본 강도로 시작하고. 독스트링이 분명히 말해. 중앙 값은 여기서 절대 덮어쓰지 않는다고. 엔진 테스트가 이걸 전부 못 박아. 슬롯 id 셋, 읽기 슬롯의 명시적 high, 두 대화의 inherit, 이름표 순서까지.

두 번째 테스트는 기능마다 자기 슬롯에서 도는지 봐. 읽기는 Kimi, talk는 Claude, kitchen은 Codex에 medium으로 심어 두고, 호출마다 실제로 뭘 받았는지 확인해. 부엌 턴은 chatgpt로 도착해. 가족 중앙에서 부르는 그 두뇌 이름과 선택기의 이름이 달라서, 킷이 둘 사이를 옮겨 주거든.

턴을 시작할 때 풀고, 짐작은 안 해

Threads._run은 턴마다 시작할 때, 그 스레드의 잠금 안에서 슬롯을 풀어. 아빠가 새 두뇌를 저장하면 대화 한가운데라도 그 기능의 다음 턴부터 바로 적용돼. 그게 되는 건 cwkPippa가 턴마다 저장된 기록으로 새로 짓기 때문이야. Master Chief Quest가 어떤 두뇌든 같은 대화라고 부르는 성질이지.

킷은 장애 앞에서 정직해. 먼저 중앙 권한에 물어. 실패하면 마지막으로 멀쩡했던 사본을 stale 표시를 달아 내주고, 설정 화면에 그렇다는 줄이 떠. 사본이 아예 없거나, 중앙이 스스로 틀렸다고 표시한 배정이면 BrainControlUnavailable을 던져. 그러면 턴은 실패로 기록되고, No brain assignment for this conversation이라는 문구가 이유와 함께 붙어. 이 대화에 배정된 두뇌가 없다는 뜻이야. 테스트 하나는 맨 처음 시작부터 권한에 못 닿게 해 놓고, 읽기가 하나도 안 도는지 확인해. 몰래 기댈 기본값은 없어.

줄 하나, 고치는 화면 둘

값 하나를 화면 둘이 고치면 부딪힐 때의 규칙이 있어야 해. 설정 화면은 슬롯의 두뇌, 강도나 inherit, 그리고 자기가 그렸던 리비전을 같이 보내. 그사이 Firelink나 다른 기기가 그 줄을 바꿨으면 쓰기가 409 충돌로 돌아오고, 페이지는 다시 불러오면서 It changed elsewhere just now. The current value is reloaded; choose again.를 띄워. 방금 다른 데서 바뀌어서 지금 값을 다시 불러왔으니 다시 고르라는 말이야. 아빠는 진짜 값을 눈앞에 두고 다시 정해. Firelink Quest의 사실 하나에 소유자 하나 트랙이 가족 규모로 하는 얘기랑 같아. 새 읽기 두뇌를 저장하면 읽기 워커가 깨서, 기다리던 접시가 곧바로 그 두뇌를 써. Ollama처럼 강도 단계가 모델마다 다른 두뇌라면, Sizzle은 단계 목록을 따로 안 들고 있어. 판단은 중앙 주인한테 넘기고, 주인은 눈에 보이는 400으로 답해. 고정 목록이 있는 두뇌는 여전히 그 목록을 지켜야 해.

재시작을 견디는 턴

턴은 돌기 전에 먼저 기록돼. ask가 저널에 turn.asked를 먼저 쓰고, 작업은 그 뒤에야 띄워. 같은 요청 id로 다시 물으면 있던 턴을 돌려줘. 그래서 답을 잃고 다시 보낸 클라이언트가 두 번 묻는 일은 없어. 부엌 테스트 하나가 같은 본문을 두 번 보내고 호출이 한 번인지 세. 스레드마다 잠금 하나가 cwkPippa 기록의 순서를 지켜.

엔진이 다시 시작하면 아직 끝나지 않은 턴이 남아 있을 수 있어. resume은 시작할 때 돌고, 절대 다시 보내지 않아. 끝나지 않은 턴마다 cwkPippa한테 그 턴의 요청 id에 묶인 저장된 답을 exact=True로 물어. 맞는 게 있으면 답으로 기록하고, 액션도 여느 때처럼 검증해. 없으면 턴을 실패로 남기고 Interrupted by an engine restart. Ask again.를 달아. 엔진 재시작으로 끊겼으니 다시 물어 달라는 말이야. 살아 있는 경로는 좀 느슨해. 자기 스트림이 막 끝난 턴은, cwkPippa가 요청 id를 되돌려주지 않았을 때 가장 최근 답을 대신 받아. 그 순간 가장 최근 답은 방금 받은 바로 그 답이니까. 재시작 뒤엔 가장 최근 답이 앞선 턴 것일 수 있어서, 정확히 맞는 것만 쳐.

다시 묻기는 새 질문이야. 버튼은 같은 말을 새 요청 id로 새로 물어 보내고, 실패한 턴은 기록에 그대로 남아. 옛 id를 그대로 쓰는 건 HTTP 답을 잃어버린 전송뿐이고, 그 재시도엔 엔진이 있던 턴으로 답해.

원칙: 일어났을지도 모르는 턴은 찾아보지, 되풀이하지 않아. 의도를 먼저 기록하고, 상대도 저장하는 id에 묶고, 죽었다 살아나면 그 id로 정확히 물어.

Code

cwkSizzle, backend/brain_control.py: 첫 등록 씨앗, 한 번의 선언, 작업 시작 때의 풀이 (원문 그대로, 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: 재시작 뒤엔 요청 id로 정확히 되찾고, 다시 보내지 않음 (원문 그대로, 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: 저장은 그렸던 리비전을 싣고, 충돌이면 다시 불러옴 (원문 그대로, 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

앱 어시스턴트의 기능 두 개에 각자 모델 설정을 줘. 화면 둘이 고칠 수 있는 공용 자리 한 곳에 둬. 씨앗은 설정마다 첫 등록 때 한 번만 심어. 작업은 시작할 때마다 하드코딩한 대체값 없이 설정을 풀어. 쓰기엔 자기가 읽은 리비전을 실어 보내서, 낡은 화면은 충돌을 받게 해. 다 되면 요청 도중에 프로세스를 죽였다가 다시 띄우고, 시작할 때 끝난 답을 요청 id로 되찾거나 요청을 실패로 남기게 해. 다시 보내는 일은 절대 없게.
Hint
호출이 나가기 전에 요청 id를 양쪽에 다 저장해. 되찾으려면 그 id로 정확히 찾아야 해. 가장 최근 답을 집어도 되는 건 내 프로세스가 아직 그 스트림을 붙잡고 있을 때뿐이야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.