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

Never Bypass, Never Resubmit

~13 min · failure handling, no fallback, timeouts, error messages

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

The two tempting moves

Picture a brain in the middle of a conversation. Dad asked for a cover. The tool call fails, or it has been running for sixteen minutes. Two moves look helpful. The first is to go around: call Ember directly, or pick an image file out of an avatar folder and use that. The second is to try again: send the same request one more time. Both are forbidden, and the reasons are different.

The Avatars client opens with both rules in its module docstring: there is no direct-Ember fallback, and a caller timeout leaves the durable batch available in Avatars rather than silently creating a second generation.

Why going around loses the picture's identity

Track 1 laid out who owns what. Avatars owns references, lineage, protection and the record of what was asked; Ember owns providers and the generated original. A direct Ember call would still produce a picture. It would produce one with no Avatars asset, no lineage, no inherited protection and no package. Ember's history would hold a row that Avatars has never heard of. If a protected soul's reference was involved, nothing downstream would know to keep that picture away from the public CDN, because the record that says so was never written.

The architecture puts it in two sentences. Reading an avatar folder and generating directly in Ember is not a supported bypass. If Avatars is unavailable, avatar generation keeps its intent and reports a retryable failure, while conversation and the serving of already published avatars carry on. Dad loses a picture for a while. Nobody loses the truth about the pictures that exist.

The client enforces the folder half in code. It refuses to write an output into my avatar folders at all: those are published copies, and new work belongs in the studio. A reference path inside those folders is never read for its bytes. The client sends the path to /internal/published-reference, which maps it to the Avatars publication it came from, or answers 404 with a sentence saying this copy has no authoritative publication.

An error message addressed to the reader

The first code block is the client's shared transport. Both of its failure messages end with the same instruction: do not bypass it with a direct image generator. origin() says it when Avatars cannot be reached from this Mac. request() says it for any other failure, a refusal included, because urlopen raises for a 4xx too. One step skips that transport. When the task is done, the original is downloaded with a bare urlopen and written to disk, outside request(). A failure there reaches ImageGen as a raw urllib or file error, with neither the instruction nor the batch id. The picture already exists by then and Avatars holds it, so the right move is the same, look at the batch in the studio and do not resubmit, but the message no longer says so.

That sentence is written for a brain. ImageGen catches the exception and returns its text as the tool result, prefixed Error: ImageGen:, and on the Claude path the result is marked as an error. So the instruction arrives inside the very message that tempts the brain to improvise. A rule in a system prompt competes with everything else the model has read. A rule in the error competes with nothing.

Principle: When a failure has one wrong reflex, put the correction in the failure itself. The reader of an error is the one about to act on it.

Why trying again is a second bill

The second code block is the end of the polling loop. While the task is still in progress, the client waits 3 seconds and asks again. Two exits are not success:

  • The task needs attention. For failed, cancelled, uncertain, copy_failed or history_missing, it raises with the batch id, the state and the task's error, and says to review or reconcile that batch and not to submit it again.
  • Time ran out. After timeout_seconds (960 by default), it raises with the batch id and the current state, and says the batch continues on the server: inspect this batch instead of resubmitting.

A timeout here is the client giving up on waiting. Avatars' worker has not given up on anything. The task may finish a minute later, saved, billed and recorded.

Now look at what a retry would send. ImageGen passes no request_id, so the client mints a fresh UUID for every call. Avatars recognises a repeated request only by its id: the same id with the same body returns the batch it already made. A second ImageGen call is therefore a new intent, a new batch and, on a paid tier, a second charge for a picture that was probably already on its way. VideoGen is different on purpose. It lets the caller pass back the request_id a refused plan returned, so confirming a clip after Dad agrees is the same request, not a new one.

What to do instead

The rule in the image-gen skill, in the third code block, says it in one line: report the existing batch ID and reconcile an uncertain outcome before retrying; do not bill a second generation. In practice:

  1. Tell Dad the batch id and the state the error named.
  2. Look before acting. /internal/batches/{id} answers the actor that asked; VideoGen has action=status for clips. ImageGen has no status action, so for an image the next look happens in the studio's batch view.
  3. Reconcile there. Track 4's lesson Unknown Is Not Failed shows how the worker searches Ember's history by task id and why an uncertain task can be retried only after a reconciliation with no match and an explicit acknowledgement.

The rules are written in five more places: the ImageGen description (do not fall back to direct generation when Avatars is unavailable), the skill, the Sidekick rulebook (reconcile before a retry, never silently regenerate), VideoGen's description (never resubmit a clip that timed out or needs attention) and the architecture. The words are there to steer a brain. The worker's retry rules are what hold when a brain does not listen.

One more line in the skill keeps the rule honest. Outside cwkPippa and Beacon, a harness may use Codex's built-in image tool by default, and the skill says plainly that this is not a service-failure fallback. A different route chosen up front is a choice. The same route chosen because Avatars failed is a bypass.

Code

cwkPippa, backend/services/avatars_client.py: both transport failures carry the instruction not to bypass (verbatim, lines 27-44)·python
def origin() -> str:
    """Where Avatars answers this process. Its ``/internal`` doors take only
    a loopback peer, so away from its Mac this is a tunnel to that loopback,
    never the tailnet twin (coop todo #631)."""
    try:
        return reach_origin(AVATARS_MEMBER, AVATARS_URL_ENV)
    except ReachError as exc:
        raise AvatarsUnavailable(f"Avatars is unreachable from this Mac: {exc}. Do not bypass it with a direct image generator.") from exc


def request(path, body=None, *, timeout=60):
    data = json.dumps(body).encode() if body is not None else None
    req = Request(origin() + path, data=data, headers={"Content-Type": "application/json"}, method="POST" if body is not None else "GET")
    try:
        with urlopen(req, timeout=timeout) as response:
            return json.load(response)
    except Exception as exc:
        raise AvatarsUnavailable(f"Avatars is unavailable or refused this action: {exc}. Do not bypass it with a direct image generator.") from exc
cwkPippa, backend/services/avatars_client.py: a stuck or timed-out batch points at itself (verbatim, lines 109-113)·python
        if task["state"] in {"failed", "cancelled", "uncertain", "copy_failed", "history_missing"}:
            raise AvatarsUnavailable(f"Avatars batch {batch['id']} needs attention ({task['state']}): {task.get('error')}. Review or reconcile that batch; do not submit it again.")
        if time.monotonic() >= until:
            raise AvatarsUnavailable(f"Avatars batch {batch['id']} is still {task['state']}. It continues on the server; inspect this batch instead of resubmitting.")
        time.sleep(3)
The image-gen skill, SKILL.md: the absolute invariants (verbatim, lines 34-43)·text
## Absolute Invariants

- Never call `google.genai` or `OpenAI().images` from the skill or workflow.
- Never read `.env` or provider keys. cwkEmber owns them.
- Never bypass Avatars or Ember after a service failure. Report the existing batch
  ID and reconcile an uncertain outcome before retrying; do not bill a second generation.
- Within cwkPippa, every brain uses the shared `ImageGen` tool through Avatars and Ember.
  The CLI below is the same client for scripts. Default `provider=auto` follows
  Ember's automatic route (`auto_route` in `--list-models`, owned by Ember; never
  restate its order). Native output preserves the original PNG; never resize it to 1024.

External links

Exercise

Find a client in your code that calls a slow or paid service and is used by an AI agent. Change it so that a timeout returns the server-side handle (job id, batch id, order id) instead of a bare failure, and so that every error message ends with the one action the reader must not take and the one it should. Then write a test that forces the timeout and asserts the handle and both instructions are in the error text.
Hint
Separate the client's patience from the job's life. The client's timeout should only end the wait; the job should keep running and stay findable by its handle. If the service has no handle to return, that is the first thing to add.

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.