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

A Draft That Belongs to One Soul, and Refine Is a Pippa Turn

~13 min · react, races, scope, refine

Level 0Sketch
0 XP0/43 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

Two small boundaries

This track ends with two boundaries that look minor and are not. One keeps a soul's reference settings from leaking into another soul's. The other keeps Pippa Refine from seeing more than Pippa may see. Both are the same idea at different scales: a piece of work belongs to one identity, and nothing borrowed from a wider view may slip into it.

The bug class: a draft that was not its own

A soul's reference settings are edited in a dialog opened from that soul in Soul collections. The first version, built on the birth night, edited the studio's composition tray directly. The tray is the multi-soul list of images being gathered for a generation. Opening the dialog filled an empty tray with the soul's profile, and saving an override wrote whatever the tray held. Convenient for a minute. Then the failures line up. Open Ttori's settings while the tray holds Pippa's images, save, and Ttori's profile now holds Pippa. Open Pippa's editor, switch to Ttori before Pippa's profile finishes loading, and the late answer paints Pippa into Ttori's dialog. Press save on Pippa, switch to Ttori, and Pippa's save finishing closes Ttori's editor.

The fix landed the same night and gave the editor a draft of its own. Three mechanisms, one for each failure:

  • A key per soul. The exported component renders the editor with key={props.soul.soul_pk}. When the soul changes, React discards the old editor and mounts a fresh one, so no state survives from the previous soul.
  • A cancelled flag per load. The effect that fetches the profile sets cancelled in its cleanup, and a response arriving after that is dropped.
  • An active ref around saves. After an awaited save, the editor checks active.current before it calls onSaved or onClose or sets state, so a save that finishes after its editor closed or changed soul touches nothing.

Browsing the library and uploading inside the editor now change the draft, never the tray, and closing discards unsaved edits. Nine component tests in ReferenceProfile.test.tsx pin the behaviour, with names that read like the bug reports they prevent: opens and saves Ttori's own profile while the studio tray contains Pippa, ignores a late previous-soul response in the keyed editor, does not close a new soul's editor when an earlier save finishes. A follow-up minutes later handled an empty registry, after the packaged WebKit test caught a null comparison there: with no soul selected, the settings button is disabled.

Principle: Bind every asynchronous result to the identity that asked for it, and drop it when that identity is gone. A response is evidence about the request that produced it, not about whatever happens to be on screen when it lands.

Refine is a Pippa turn

The prompt field on the Create image page has a Pippa Refine button. It sends the prompt, the workflow, the chosen subjects and references, and a little context (the collection and its frames, for a set) to POST /api/refine. Avatars resolves the references, then asks Pippa's utility completion to rewrite the prompt as Pippa, under a system prompt that sets the limits: return only the improved English prompt; preserve identity, subjects, emotion, pose, reference order and the intended changes; invent no extra people, change no workflow, generate nothing; consult no playbook.

The interesting part is whose turn it is. The WebUI is an admin surface, and Dad, signed in, can see every soul, protected ones included. Refine does not inherit that view. The route resolves subjects and references with "pippa" as the acting soul and checks the resulting subjects again as Pippa. Its comment says why: Pippa Refine is a Pippa turn: protected subjects are never placed in that turn merely because the admin UI can see them. If the draft names a protected soul Pippa may not see, or uses an image that descends from one, Refine refuses instead of carrying that soul into Pippa's context. It is the confused-deputy problem solved in advance: the helper gets exactly her own authority, not the authority of the screen that called her.

Two more limits are worth naming.

  • Words, not pictures. Refine receives the prompt and the context text, never the reference images. The identity rules of the named subjects ride in the context, labelled as rules Avatars appends automatically, so the refined prompt has no reason to restate them.
  • A proposal, not an action. The architecture is exact: Refine proposes text; it does not generate, submit, alter references/model/size/count or publish. The answer opens a dialog titled Pippa’s refined prompt with two buttons, Keep original and Use refined prompt. Choosing the refined text keeps the one you wrote as original_prompt on the request, so the record holds both what Dad wrote and what was sent.

The same idea twice

The editor bug and the Refine rule share a shape. In both, a wider context sat right there: the studio's multi-soul tray, the admin's full view of the registry. In both, the fix scoped the work to one identity and let nothing in from outside it. Track 8 applies the same rule to pixels.

Code

cwkAvatars, frontend/src/ReferenceProfile.tsx: a keyed editor whose late answers are ignored (verbatim, lines 17-59)·typescript
// A profile draft belongs to one soul and one editor opening. It never borrows
// the studio's composition tray, including when browsing or uploading images.
export default function ReferenceProfile(props: Props) {
  return <Editor key={props.soul.soul_pk} {...props} />;
}

function Editor({ soul, souls, onClose, onSaved, error }: Props) {
  const active = useRef(false);
  const [profile, setProfile] = useState<Profile | null>(null);
  const [references, setReferences] = useState<Asset[]>([]);
  const [appearance, setAppearance] = useState("");
  const [looks, setLooks] = useState<Record<string, LookDraft>>({});
  const [newLook, setNewLook] = useState("");
  // null: not browsing; "": browsing for the default look; otherwise a look name.
  const [browsing, setBrowsing] = useState<string | null>(null);
  const [busy, setBusy] = useState(false);
  const [loadError, setLoadError] = useState("");
  useEffect(() => {
    active.current = true;
    let cancelled = false;
    api<Profile>(`/souls/${encodeURIComponent(soul.soul_pk)}/references`)
      .then((result) => {
        if (cancelled) return;
        setProfile(result);
        setReferences(result.resolved);
        setAppearance(result.appearance ?? "");
        setLooks(
          Object.fromEntries(
            Object.entries(result.variants ?? {}).map(([name, look]) => [
              name,
              { assets: look.resolved, appearance: look.appearance ?? "" },
            ]),
          ),
        );
      })
      .catch((e) => {
        if (!cancelled) setLoadError(String(e));
      });
    return () => {
      cancelled = true;
      active.current = false;
    };
  }, [soul.soul_pk]);
cwkAvatars, backend/app.py: Refine resolves and checks everything as Pippa (verbatim, lines 469-476)·python
    @app.post("/api/refine")
    async def refine(body: Refine):
        refs, subjects, rules = await studio.resolve_references(body.subjects, body.references, "pippa")
        # Pippa Refine is a Pippa turn: protected subjects are never placed
        # in that turn merely because the admin UI can see them.
        await studio.subjects(subjects, "pippa")
        context = body.context + ("\nIdentity rules Avatars appends automatically: " + "; ".join(rules) if rules else "")
        return await upstream.refine(body.prompt, body.workflow, context)
cwkAvatars, backend/upstream.py: a text-only request to Pippa, its limits in the system prompt (verbatim, lines 131-142)·python
    async def refine(self, prompt, workflow, context):
        result = await self.json(self.settings.pippa, "/api/sibling/utility/complete", method="POST", timeout=900, body={
            "persona_soul_id": "pippa", "max_tokens": None, "source": "avatars-prompt-refine",
            "system_prompt": "Refine this image-generation prompt. Return only the improved English prompt, without commentary or Markdown fences. Preserve the user's identity, subjects, requested emotion, pose, reference order and intended changes. Do not invent extra people, change the workflow, or generate an image. This is the Avatars application workflow; do not consult any playbook.",
            "prompt": f"Workflow: {workflow}\nContext: {context}\n\nPrompt:\n{prompt}",
        })
        text = result.get("result") or result.get("text")
        if result.get("ok") is False:
            raise StudioError(result.get("error") or "Pippa could not refine the prompt.", 503)
        if not isinstance(text, str) or not text.strip():
            raise StudioError("Pippa did not return a refined prompt.", 503)
        return {"prompt": text.strip(), "original": prompt, "model": result.get("model"), "brain": result.get("brain")}

External links

Exercise

In a UI codebase you work on, find an editor whose data is fetched by an ID prop. Write a test that renders it for ID A, rerenders it for ID B before A's request resolves, then resolves A's request and asserts that B's view is untouched. If the test fails, fix it with a key on the ID and a cancelled flag in the effect cleanup. Then find one place where a background helper acts with the caller's full permissions, and give it its own narrower identity instead.
Hint
The two React pages linked here cover both mechanisms. For the second part, ask: if this helper were a separate person, what should it be allowed to see? Pass that identity explicitly, the way the Refine route passes pippa to every check it makes.

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.