A scheduler prints step 4 failed at 02:14. The wrapper does what a tired script usually does: it resubmits the same prompt. The first host is busy, so the job lands somewhere else. A helper also swaps the model route, because a free endpoint answered first.
The new diff is green. The allowlist was not read again. Nothing in the log says whether the failure was a timeout, a schema miss, or a tool call outside the named surface.
That sequence is not a retry. It is three decisions collapsed into one button. This walkthrough splits them into a glossary, an ordered tree, and a function you can run on local rows.
The scenario is a composite, used to show the branches. It is not a report from a named fleet, and it carries no measured failure rate.
Terms the tree will accept
Use the same words on the job card, the log line, and the review note. If a field is missing, the tree has no input, and the only legal output is stop.
Step binding. The host id, the model route id, and the input snapshot id for one attempt. Change any field and you have a new binding, not a second try of the old one.
Input snapshot. A content id for the prompt, the tool schema, and the fixture ids the step may read. How you compute that id is out of scope here. The tree only needs equality: same snapshot, or not.
Failure class. A closed label for why the attempt missed its contract. unknown is allowed. It is also a stop, not a prompt to let another model invent the label.
Same-binding retry. Same host, same route, same snapshot. Only the attempt counter moves.
Compute move. The host changes. The route and the snapshot stay. Use it for reachability, not for a better answer.
Route narrowing. The route changes to a smaller, tools-off, read-only call. The reply is stored as a sketch. Accepting that sketch is a later job, not this leaf.
Halt. No socket is opened. A person reads the attempt record before any new binding exists.
Attempt record. One row: failure_class, attempt, writes, tools_enabled, snapshot_same. The function below reads only those fields.
Checks, in order
First match wins. Do not also move the host because a later leaf looks cheaper.
- If
failure_classis empty orunknown, return halt. A missing label is not repaired by another call. - If
failure_classisscope_creep, return halt. A write or tool call outside the allowlist is not a transport fault. A new host would copy it. - If
writesortools_enabledis true, and the class iscontract_missorscore_wobble, return halt. A writing step that failed its schema does not become safe on a narrower route. - If the class is
transport,attemptis 1, and the snapshot matches, return same-binding retry. One quiet resubmit. Not a loop. - If the class is
transport,attemptis greater than 1, and the snapshot matches, return compute move. Keep the model route string byte-for-byte. - If the class is
contract_miss,writesis false, and tools are off, return route narrowing. Ask for one labeled field. Do not turn tools back on so the model can look around. - Every other row returns halt. That includes
score_wobbleon a read-only step. Two scores are a record. They are not a cue to sample until one looks clean.
One worked row per leaf
The rows are illustrations. They are not excerpts from a production log.
Halt, unknown class. {failure_class: "unknown", attempt: 1, writes: false, tools_enabled: false, snapshot_same: true}. Open the raw stderr and assign a class by a fixed rule. Do not spend a free call to justify the next free call.
Halt, scope creep. {failure_class: "scope_creep", attempt: 1, writes: true, tools_enabled: true, snapshot_same: true}. Freeze the card. The next machine must not receive this prompt until the allowlist and the diff have been read. A spare server is still a server.
Halt, writing contract miss. {failure_class: "contract_miss", attempt: 1, writes: true, tools_enabled: false, snapshot_same: true}. Keep the bad output beside the schema error. A second sample can parse and still name the wrong path.
Same-binding retry. {failure_class: "transport", attempt: 1, writes: false, tools_enabled: false, snapshot_same: true}. Resubmit once. Same host id, same route id. If the counter would become 3, this leaf no longer applies.
Compute move. {failure_class: "transport", attempt: 2, writes: false, tools_enabled: true, snapshot_same: true}. Select a host that can read the same snapshot and the same route. Tools stay as they were. You moved compute. You did not relax policy.
Route narrowing. {failure_class: "contract_miss", attempt: 1, writes: false, tools_enabled: false, snapshot_same: true}. Send a shorter prompt that requests a single field, with tools disabled. Store the reply as a sketch. Promotion stays a separate card.
Halt, score wobble. {failure_class: "score_wobble", attempt: 2, writes: false, tools_enabled: false, snapshot_same: true}. Keep both numbers. Do not average them. Do not call again to break the tie.
A snapshot mismatch on a transport failure also halts. If the bytes changed, you are not retrying. You are running a different step.
| failure_class | attempt | writes | tools | snapshot_same | leaf |
|---|---|---|---|---|---|
| unknown | 1 | false | false | true | halt |
| scope_creep | 1 | true | true | true | halt |
| contract_miss | 1 | true | false | true | halt |
| transport | 1 | false | false | true | same_binding_retry |
| transport | 2 | false | true | true | compute_move |
| contract_miss | 1 | false | false | true | route_narrowing |
| score_wobble | 2 | false | false | true | halt |
| transport | 2 | false | false | false | halt |
Run the leaves locally
This function is a proposal. It does not open a network connection. It does not know which vendor string you will later attach to a leaf. Run it before you trust the table above; the assertions are the table, encoded.
from dataclasses import dataclass
@dataclass(frozen=True)
class Attempt:
failure_class: str
attempt: int
writes: bool
tools_enabled: bool
snapshot_same: bool
def decide(row: Attempt) -> str:
if row.failure_class in {"", "unknown"}:
return "halt"
if row.failure_class == "scope_creep":
return "halt"
if (row.writes or row.tools_enabled) and row.failure_class in {
"contract_miss",
"score_wobble",
}:
return "halt"
if (
row.failure_class == "transport"
and row.attempt == 1
and row.snapshot_same
):
return "same_binding_retry"
if (
row.failure_class == "transport"
and row.attempt > 1
and row.snapshot_same
):
return "compute_move"
if (
row.failure_class == "contract_miss"
and not row.writes
and not row.tools_enabled
):
return "route_narrowing"
return "halt"
def test_leaves() -> None:
assert decide(Attempt("unknown", 1, False, False, True)) == "halt"
assert decide(Attempt("scope_creep", 1, True, True, True)) == "halt"
assert decide(Attempt("contract_miss", 1, True, False, True)) == "halt"
assert decide(Attempt("transport", 1, False, False, True)) == "same_binding_retry"
assert decide(Attempt("transport", 2, False, True, True)) == "compute_move"
assert decide(Attempt("contract_miss", 1, False, False, True)) == "route_narrowing"
assert decide(Attempt("score_wobble", 2, False, False, True)) == "halt"
assert decide(Attempt("transport", 2, False, False, False)) == "halt"
Save it as retry_tree.py. Then:
python -m py_compile retry_tree.py
python -c "import retry_tree; retry_tree.test_leaves(); print('leaves_ok')"
py_compile only checks syntax. The second command runs the eight assertions. If one fails, the table and the function have drifted. Edit the table before you edit the function. Do not delete a leaf to make the command quiet.
Two leaves can name a vendor option
The other leaves stay vendor-free. Compute move may set the new host id to a free server when the operator has confirmed that option. The model route does not change. Route narrowing may set the route to free model access when that option is confirmed, and only for a read-only call with tools off.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode is relevant only on those two leaves, and only if the current offer matches what the operator confirmed. This article does not state model names, token quotas, hardware, regions, or duration. Those facts go stale quickly, and a hard-coded quota in a job card is worse than a missing host id. Read the operator's current docs, then write the host id and the route id into config. If either option is absent, point compute move at any approved spare host and point route narrowing at any approved smaller route. The tree still holds.
Limits of the function
decide does not parse stderr. Something else must set failure_class before this runs. Prefer a deterministic map: timeout with an empty body is transport; a schema error is contract_miss; an allowlist miss is scope_creep; a changed score on an unchanged snapshot is score_wobble. If that map is itself a model call, its mistakes become silent halts or silent moves.
The same-binding leaf allows one extra attempt. A disk that fails twice is a ticket, not a third call. Raise the cap only with a written reason in the attempt record, not with a while loop in the worker.
Compute move assumes the new host can address the same snapshot. A snapshot that lives only on the first machine makes the leaf invalid. Return halt until the snapshot is published to a path both hosts can read.
Route narrowing refuses tools and writes. A task that cannot be asked as a single labeled field does not get that leaf. It gets halt. Shrinking the prompt until it fits is a review decision, not a retry-worker decision.
Who should not use this
Skip the tree if attempt records do not already store a write bit and a tool bit. Guessing those flags inside the retry script rebuilds the one-button wrapper this walkthrough is trying to retire.
Skip it for interactive chat. There is no step boundary, no allowlist, and no snapshot id. The leaves assume a job card.
Skip compute move when policy forbids a data set on a shared or free host. The function cannot see that policy unless you encode the denial as halt before decide runs. Do not add a silent exception for a single job.
Skip it if you need a quality comparison between models. Nothing here measures accuracy, latency, or cost. A green leaves_ok line means the branches match the table. It does not mean a route is good.
Put the halt in front of the socket
Store the eight rows as fixtures, and call decide before the worker opens a connection. On compute_move or route_narrowing, copy the host or the route from operator-confirmed config, and write the new binding beside the old one. On halt, the next artifact is a review note. Another call is the failure mode, not the fix.
Top comments (0)