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

What Cannot Be Cancelled, and Material Not Avatar

~14 min · cancellation, reconciliation, lineage, playback

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

Cancel stops at the vendor's door

An image job in flight can at least be asked to stop. cancel() posts Ember's cancel and answers honestly that a metered request already sent may still be billed. A clip gets no such request. Once a clip is running, the answer is a 409: A submitted clip cannot be cancelled: the vendor finishes it, charges it, and it is saved. Ember's own cancel says the same from its side and accepts only a clip that has not been submitted.

So a clip's cancel is meant to work only where it is free. A task still queued or preparing is cancelled locally. Usually that is the whole story, because nothing has been sent. There is one way it is not. When a submit's answer is lost to a transport error or a 5xx, the worker puts the clip back in queued to re-send under the same key (the previous lesson), and a restart does the same with a clip left in submitting. Ember may already have accepted that request. Cancel then changes only Avatars' row: Ember's clip runs on and is charged, Avatars stops looking for it, and a retry from cancelled runs under the next attempt's key, which can pay for a second clip. A task submitting or uncertain is refused with Reconcile this task first, because nobody knows what there is to cancel. The review dialog says it before Dad presses the button: a submitted clip cannot be cancelled. Even shutting the worker down follows the rule. stop() ends the polling and never cancels authorized upstream work.

Waiting, and a sentence for every ending

While Ember reports the clip as queued, submitting, generating or downloading, the worker polls GET /api-video/jobs/{id} every 5 seconds and copies Ember's phase onto the task. A poll that errors is only a read, so the worker waits and asks again. A 404 is different. Ember no longer knows the clip, so the task becomes uncertain, with a warning that a retry pays for a new clip. When polling ends, each ending gets its own words:

Ember saysThe task becomesThe task says
failedfailedThe vendor's reason, then Nothing was charged.
cancelledcancelledCancelled before submission
donecopying, then doneSaving the clip
anything elseuncertainResolve it in Ember's Pro Video tab, then reconcile here; never retry blindly.

In Ember's contract, failed means the vendor refused or could not generate the clip, and nothing was charged. The test's example is a moderation refusal, which the task reports as The prompt was rejected by content moderation. Nothing was charged. Uncertain means money may have been spent and Ember cannot say, as with a finished clip that would not download. Avatars refuses to guess and points Dad at the one place that can decide.

Reconcile asks, never re-sends

reconcile_clip() has a sharper tool than the image search, because every attempt has a name. It asks Ember by clip id when a submission was acknowledged. Otherwise, or after a 404, it asks by the attempt's key through GET /api-video/requests/{key}, rebuilding the key of the attempt that was accepted. It never posts. Ember's answer moves the task:

  • No clip for that key. Nothing reached the vendor. The reconciliation is recorded with zero matches, and an acknowledged retry may now pay for a new clip.
  • Done. The task goes to copying and saves the clip. Nothing is generated again.
  • Still pending. Back to running, and the worker waits again.
  • Failed. failed, nothing charged.
  • Still uncertain in Ember. The clip id is recorded as a match, so retry() keeps refusing until Ember resolves it.

The test plays the hard case. Ember holds a clip as uncertain because its download failed. A retry is refused before reconciling, and again after, because the match stands. Once Ember fetches the download again, reconciling moves the task to copying, the worker finishes it, and Ember has still received exactly one request.

Material, never an avatar

When Ember reports the clip done, finish_clip() fetches the MP4 and its poster frame and passes both to save_clip(). The store checks that bytes 4 to 7 read ftyp, the box an MP4 file opens with, then names the file by its SHA-256 and writes it once, exactly as it writes an image original. The poster goes beside the thumbnails as a derivative that can be made again, and a clip whose poster could not be fetched is still saved. The asset carries media: "video", the duration and pixel size, the settings, the estimate, the charge, Ember's clip id, and the subjects and references it was made from.

That lineage is why Avatars keeps a copy at all, when Ember is every clip's home. It lets Super Soul protection follow a clip through its subjects and references, as the Protection Follows the Pixels track explains. And a clip is walled off from everything an avatar is. A reference profile refuses it, an image plan refuses it, a clip's own references and frames refuse it, and every export preset refuses it. The architecture doc calls a clip material for Dad's channel.

The charge, read back

No vendor answer carries a price, so the charge arrives late. Ember reads it from the vendor's usage analytics after waits of 20, 60, 180 and 600 seconds, about 14 minutes in all. Each time the batch detail is read, Avatars asks Ember once more for any clip whose copy has no charge yet, for up to 30 minutes after saving. After that, in the comment's words, Ember stopped looking long ago and the estimate stands. The studio shows the charge when it is known and about N credits until then. The one clip measured end to end, Pippa by a fireplace on Veo 3.1 Fast at 1280×720 for 8 seconds, took 88 seconds and charged 4,848 credits, exactly the estimate.

Playing it where it lives

A clip lives on the service host, so that is where it plays. POST /api/assets/{id}/play refuses anything that is not a clip, looks the file up in the store and opens it with open -b com.cwk.ashenreel, Ashen Reel's bundle id. The caller names an asset, never a path. The answer names the Mac by its fleet label, so the lightbox's Open in Ashen Reel button can say where the clip appeared. Ashen Reel Quest teaches the same rule from the player's side: a handoff carries an identity, never a path. Inline playback on iPhone and iPad belongs to The Studio in Your Pocket.

Principle: When an action cannot be undone, say so before it starts, refuse to pretend after, and keep asking the side that knows.

Code

cwkAvatars, backend/worker.py: cancel is local before submission, refused after (verbatim, lines 243-254)·python
    async def cancel(self, id):
        task = await self.studio.io("get", id, "task")
        if task["state"] in {"queued", "preparing"}:
            return await self.studio.io("update", id, expected=task["version"], state="cancelled", phase="Cancelled before submission")
        if task["state"] in {"submitting", "uncertain"}:
            raise StudioError("Reconcile this task first; its upstream outcome is not known.", 409)
        if task["state"] != "running":
            raise StudioError("This task is no longer cancellable.", 409)
        if is_clip(task):
            raise StudioError("A submitted clip cannot be cancelled: the vendor finishes it, charges it, and it is saved.", 409)
        await self.studio.upstream.json(self.studio.upstream.settings.ember, f"/api/v1/api-generators/jobs/{task['job_id']}/cancel", method="POST", body={})
        return {"requested": True, "message": "Cancellation requested. A metered request already sent may still be billed by the provider."}
cwkAvatars, backend/worker.py: reconcile by clip id or by the attempt's key, never by re-sending (verbatim, lines 284-308)·python
    async def reconcile_clip(self, task):
        """Ask Ember what became of this clip: by its id when the submit was
        acknowledged, else by the attempt's request id. Nothing is re-sent."""
        io, upstream, id = self.studio.io, self.studio.upstream, task["id"]
        record = None
        if task.get("job_id"):
            try:
                record = await upstream.video_clip(task["job_id"])
            except StudioError as exc:
                if exc.status != 404:
                    raise
        if record is None:
            attempt = {**task, "attempts": task["attempts"][:-1] if task.get("job_id") and task["attempts"] else task["attempts"]}
            record = await upstream.video_request(clip_request_id(attempt))
        if record is None:
            await io("update", id, phase="Reconcile before retry", reconciliation={"checked_at": now(), "matches": []}, error="Ember has no clip for this task, so nothing was submitted to the vendor. A retry makes and pays for a new clip.")
        elif record["state"] == "done":
            await io("update", id, state="copying", phase="Saving the clip", job_id=record["id"], upstream_result=record, error=None)
        elif record["state"] in CLIP_PENDING:
            await io("update", id, state="running", phase=record.get("phase") or "Generating", job_id=record["id"], error=None)
        elif record["state"] == "failed":
            await io("update", id, state="failed", phase="Generation stopped", job_id=record["id"], error=f"{(record.get('error') or {}).get('message') or 'The vendor could not generate this clip.'} Nothing was charged.")
        else:
            await io("update", id, phase="Reconcile before retry", job_id=record["id"], reconciliation={"checked_at": now(), "matches": [record["id"]]}, error=f"{(record.get('error') or {}).get('message') or 'Ember holds this clip as uncertain.'} Resolve it in Ember's Pro Video tab first.")
        return await io("get", id, "task")
cwkAvatars, backend/store.py: a clip kept like any original (verbatim, lines 189-204)·python
    def save_clip(self, data: bytes, *, label: str, subjects: list[str], width: int | None, height: int | None, duration: float | None, poster: bytes | None = None, references: list[str] | None = None, **metadata) -> dict:
        """A generated video clip, kept like any original: native bytes,
        content-addressed, with the lineage of the references it was made from.
        Its poster frame is a regenerable derivative beside the thumbnails."""
        if len(data) < 12 or data[4:8] != b"ftyp":
            raise StudioError("The clip is not an MP4 file.")
        digest = hashlib.sha256(data).hexdigest()
        filename = f"{digest}.mp4"
        self.write_original(filename, data, digest)
        if poster:
            target = self.root / "thumbnails" / f"{digest}.poster.jpg"
            if not target.exists():
                temporary = target.with_name(f".{identity()}.tmp")
                temporary.write_bytes(poster)
                os.replace(temporary, target)
        return self.add("asset", label=label[:200], subjects=sorted(set(subjects)), origin="generated", references=list(dict.fromkeys(references or [])), filename=filename, sha256=digest, width=width, height=height, duration=duration, mime="video/mp4", media="video", bytes=len(data), archived=False, **metadata)

External links

Exercise

Pick a paid, irreversible operation in your own software. Write its cancel as a table of states: where cancel is free and certain, where it must be refused until the outcome is known, and where it is refused for good with a sentence saying what will happen instead. Then write its reconcile so that it only reads from the other side, by the id you were given or by your own idempotency key, and moves the task to exactly one next state for each answer. Finally, store the result with a check on its file signature and with the inputs it was made from.
Hint
List the states your worker can be in before writing any code, and give every pair of state and remote answer its own row. A reconcile that can post anything is a retry in disguise. For the signature check, read the first dozen bytes of a real file in a hex viewer and decide which bytes you will trust.

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.