Three possible answers
Most code treats a remote call as a yes or a no. For a call that may spend money, that is one answer short. After the worker posts a task to Ember, exactly one of three things is true:
- Ember said no. A 4xx is an explicit rejection: Ember read the request and refused it, so nothing was generated. The task becomes
failedwith the phase Submission refused and Ember's own sentence as the error. - Ember said yes. A 202 with a job id. The task becomes
runningand the id is recorded. - Nobody knows. The connection dropped, the call timed out, Ember answered with a 5xx, or the answer could not be read. The comment beside that line in the worker: transport errors and 5xx cannot prove whether a metered request was accepted. The task becomes
uncertain, phase Reconcile before retry.
The plumbing makes that split easy to get right. Avatars' upstream client turns every transport failure and every unreadable body into a 503, and turns any 5xx Ember sends into a 503 as well, while a 4xx keeps its own status and carries Ember's message. So the worker needs one comparison: 400 up to 499 is a refusal, everything else it cannot vouch for.
Where unknowns come from
Submission is only the first place an outcome can go missing. The worker files every other doubt the same way:
| What happened | Resulting state |
|---|---|
The engine restarted while a task was submitting | uncertain |
| Polling hit a transport error, a non-200 status, or the 1800-second bound | uncertain |
| Ember reported the job failed | failed |
| Ember reported the job cancelled | cancelled |
| Ember answered 404 for a job it had accepted | reconciled at once (below) |
| Fetching the original from Ember failed, after Ember's answer was saved | uncertain |
| Anything else, after Ember's answer was saved (writing the asset, say) | copy_failed |
| Anything else, after a job id existed or mid-submit | uncertain |
| Anything else, before anything was sent | failed |
The download row is easy to misread. It looks like a copy problem, but the kit's client raises a failed image fetch as an EmberJobError, and the worker files every such error that is not Ember's own report of a failed or cancelled job as uncertain. Ember's answer is still saved on the task, so a reconcile finds the same history row and runs the copy again. Nothing is generated.
The 404 case is worth a sentence. Ember keeps live job records in memory, so after Ember restarts, a job it accepted an hour ago simply is not there. The Sibling Kit's client reads that 404 as the job being gone, never as still running. The worker does not wait for a person: it marks the task uncertain, phase Reconciling with Ember history, and reconciles straight away, because Ember's archive outlives its memory.
Reconcile by identity
Every job Avatars submits carries its own task id inside source_metadata, and Ember stores that metadata with the history row of each image it makes. Reconciling is a search for that id. reconcile() pages through Ember's Pro history a hundred rows at a time and collects every row whose metadata names this task. Then:
- One match. That row is the result. The worker stores it as Ember's answer and runs the normal finish: copy the original, release protection, mark done. No generation is repeated.
- Several matches. It stops, records the match ids and says Multiple archived outputs match this task. Review them before retrying.
- No match. The task stays
uncertainwith a recorded reconciliation (when it checked, and an empty match list) and the sentence No Pro history match found. The upstream request may still be running; an explicit retry can create another billed image.
Zero matches is information, and it is still not proof. A job that is generating right now has no history row yet. That is why the empty result is recorded with its time and the sentence keeps both possibilities open. A copy_failed task with Ember's answer already saved needs no search at all; reconcile sends it back to copying.
The batch view offers the same thing in bulk. Reconcile N with Ember runs it over every task that needs it and reports how many were recovered from history, how many still need a decision, and how many could not be checked. It never retries anything.
Retry is a decision
retry() accepts a failed or cancelled task without a reconciliation: Ember has reported the job's end, so there is nothing left to reconcile. That is not proof that nothing was spent. Ember's comment beside its vendor calls says a call already sent cannot be aborted: cancelling only stops Ember from waiting, the call runs to completion in the background, may still be billed, and its result is discarded. So retrying a cancelled task can pay a second time for work the vendor already finished. An uncertain task needs three things together: a recorded reconciliation, zero matches in it, and the caller's explicit acknowledge_uncertain. Without all three the answer is a 409, Reconcile history and explicitly acknowledge the uncertain outcome before retrying. In the batch view that acknowledgement is a separate button, Retry despite uncertain outcome, behind a confirmation that says the prior request may still finish and be billed. A history_missing or copy_failed task is never retried as a new generation; its image already exists.
A retry keeps the past. The old state, error and reconciliation are appended to attempts, the job id is cleared, and the task goes back to queued. Subject policy is checked again first, because a subject may have become protected since.
Cancel tells the truth too
Before submission, cancel is local and certain. While submitting or uncertain, it is refused, because nobody knows what there is to cancel: Reconcile this task first; its upstream outcome is not known. While running, it asks Ember to cancel and answers honestly: Cancellation requested. A metered request already sent may still be billed by the provider.