DEV Community

Taylor Lin
Taylor Lin

Posted on

Branch on Failure Class Before the Host or the Model Moves

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.

  1. If failure_class is empty or unknown, return halt. A missing label is not repaired by another call.
  2. If failure_class is scope_creep, return halt. A write or tool call outside the allowlist is not a transport fault. A new host would copy it.
  3. If writes or tools_enabled is true, and the class is contract_miss or score_wobble, return halt. A writing step that failed its schema does not become safe on a narrower route.
  4. If the class is transport, attempt is 1, and the snapshot matches, return same-binding retry. One quiet resubmit. Not a loop.
  5. If the class is transport, attempt is greater than 1, and the snapshot matches, return compute move. Keep the model route string byte-for-byte.
  6. If the class is contract_miss, writes is 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.
  7. Every other row returns halt. That includes score_wobble on 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"
Enter fullscreen mode Exit fullscreen mode

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')"
Enter fullscreen mode Exit fullscreen mode

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)