0016 — A model that cannot be reached is not a model that declined
- Status: Accepted (the rule). The mechanism is in gera-propflow/local-bin#109, open, not merged —
fable-decidegrows a second model rung,blockedstops attributing opinions to models that did not give them, and the operator skill's exit table catches up. This ADR does not wait on that merge, because ADR-0004 puts the record at decision time. ⚠️ SUPERSEDED-BY-0019 FOR THE ORDERING CLAUSE — ADR-0019 asks Fable and Astra concurrently and drives on agreement, so line 9'sFable → Astra → a humanladder, line 57's "Astra answers only when Fable could not", and line 70's rejection of asking both models no longer state the rule. Everything else here remains Accepted and load-bearing, and ADR-0019 rests on it: rules 1–3 (reached ≠ declined), rule 4 (the two-pass reversal guard and the class gate), rule 5 (provenance), rule 6 and the exit table — in particular exit 3 NEEDS HUMAN is terminal and does NOT fall through to Astra; only unreachability does (lines 30–34). Do not act on line 9 without reading ADR-0019. ⚠️ Line numbers cited here and in ADR-0019 are this file's ORIGINAL numbering and this amendment deliberately adds no line — keep it that way. ⚠️ VOCABULARY: the role this ADR calls the Operator is the Driver since 2026-09-10 —operatoris the product's word for a PM company. Decision text below is unchanged; ids, binaries, trigger phrases and theOperatorTaskWorkflowtype that still sayoperatorare live invocations and deliberately lag. See ADR-0021. (Appended toStatuson purpose: this adds NO line, so every line-number citation into this file stays true.) - Date: 2026-09-10
- Deciders: Gera, 2026-09-10, after five questions on PR #7618 spent ninety minutes reported as "pending Fable": "If Fable's not available then it should use Astra." Written at decision time by the Worker in the room, because ADR-0004 says a decision that changes how systems relate is not done until it is written here.
The decision
The decision ladder is Fable → Astra → a human, and the fall-through is automatic.
- A rung that CANNOT BE REACHED falls through to the next one. A capped seat, an expired bearer, a missing binary, a timeout, a non-zero exit, a reply with no json in it. These are missing evidence, and missing evidence is not a hard question.
- A rung that WAS REACHED AND DECLINED does not.
resolvable: false, a flip between the two reversed passes, or the money/commitment class gate firing — all three are verdicts, and all three go to the human rung. Falling one through would launder a refusal into a ruling. - These two must never share an exit path. This is the whole ADR. The discriminator is one pure function and it reads exactly one thing: did any pass of this rung return a parseable verdict object?
- The two-pass, options-reversed discipline and the class gate travel with the ladder. Whichever rung answers asks twice, concurrently, with the options in opposite order, and reports a flip as unresolved. A fallback that dropped the guard would be a cheaper answer wearing the costume of the expensive one.
- Every surface that carries the answer names the model that produced it. The receipt's
modelfield is the model that answered, not the one asked first;answered_byandfallback {from, to, reason}sit beside it; the terminal printsRESOLVED via ASTRA (gpt-6-astra)with a banner; and the decision page's receipt card says which model was asked and whether the question fell through. - When no model rung can answer, the tool fails fast. Exit 4 WALLED means "the ladder is down to the human — go there now". Exit 2 ERROR is reserved for the case where at least one rung failed in a way that genuinely clears, and its receipt carries
retry_afterso the retry is bounded.
Why — 2026-09-10, in local time (CDT)
At 14:26 a retry job armed against fable-decide began logging seat session_5h pct=101.0 every sixty seconds, with zero progress. It ran that way for about ninety minutes. Five questions on PR #7618 sat behind it reporting "pending Fable".
Nothing was broken in the retry job. It was waiting on a condition that does not clear for hours: six of seven fleet seats were at 100–101% of their weekly Fable allowance, and the seventh — the only one with weekly headroom — was session-capped until 20:40Z. The seat picker read weekly_fable and nothing else, so every call routed to the one seat in the fleet guaranteed to fail. It failed saying "You've hit your session limit · resets 3:40pm (America/Chicago)" — "hit your", not "reached your", one verb outside the wall detector — so the run fell through to exit 2, which the tool documents as "retry, do not escalate".
local-bin#107 landed at 14:48 and fixed the classification: a session cap is now exit 4 WALLED, a durable state, and a caller that sees it is told to stop retrying. That converts a spin into a stop — but it stops at "go ask a human." The ninety minutes were still spent, and they were spent while codex exec -m gpt-6-astra was up the entire time. Once a human noticed and redirected the same questions, Astra answered them in about fifteen minutes.
So the gap this ADR closes is not the classification. It is that the ladder had no rung between "Fable is unavailable" and "wake a person", and a human had to be the one to notice.
Why the reached/declined distinction is the load-bearing half
The obvious implementation — "if fable-decide exits non-zero, try Astra" — is wrong in a way that is hard to see and expensive when it fires. fable-decide exits 3 for NEEDS HUMAN, and NEEDS HUMAN is the answer the brief expressly invites:
"Saying 'a human must decide this' is a CORRECT and useful answer, not a failure."
A model saying only a person can settle a question, and a model that was never asked because its meter refused, are opposite facts. Under a single fall-through path they become the same event: a question Fable declined to settle would be handed to Astra, could come back RESOLVED, and would be acted on — with nothing on the page saying a refusal had been overruled by a second model. The class gate would still catch the money cases; nothing would catch a preference, a risk appetite, or a promise made to a person.
The distinction survived contact with a real ordering bug. fable-decide's existing code checked "did a pass fail to run?" before "did a pass refuse?" — so a pass answering resolvable: false while its twin timed out was reported ERROR. That was one wasted turn before this ADR; with a fallback behind it, ERROR falls through, and it would have been a laundered refusal. The refusal check now runs first.
Why Astra, and not another Fable seat
Fable's two meters are per-account, and the fleet shares an account pool. When the weekly allowance is spent it is spent for every seat at once — a fallback inside the same vendor is walled by the same wall. Astra is a different vendor, different weights, different meter, and it is already on this machine and already authed. It was measured answering in 7 seconds during the incident.
Why "a fallback is a cheaper answer" is a real risk, and how it is bounded
Two guards make fable-decide's verdicts worth more than a single model call: the options-reversed second pass (position bias), and the class gate that overrides agreement on money and commitments (paid for on 2026-08-07 by the unit-607 renewal rate, RESOLVED by both passes at high confidence and wrong). A fallback rung that ran one pass, or skipped the gate, would produce verdicts that look like the first rung's and are not. Both guards run on whichever rung answers, and a control asserts each — including one that asserts the two Astra passes really do get reversed briefs, using a stub that behaves like a position-biased model rather than a canned pair of answers. The canned version passed against a mutant that gave both passes the same brief; that is why it is written the way it is.
What changes for whom
| Reader | Before | After |
|---|---|---|
An Operator running fable-decide |
exit 4 meant "Fable's meter refused; go to a human" | exit 4 means "no model rung can answer; go to a human". A question Fable cannot take is answered by Astra without anyone noticing it needed to be. |
| Anyone reading a verdict | it was Fable's, always | read answered_by. The terminal line and the receipt both name the model. |
| Anyone reading a decision page | the card said "Fable recommends" over the raiser's own --rec, and the receipt block said "Fable was asked" |
in the page blocked renders: the recommendation reads "The session raising this recommends", and the receipt block names the model that answered. ⚠️ The agents-page Decisions tab is a SECOND renderer and still says it — propflowai/src/app/(workspace)/(operations)/agents/_components/DecisionsTab.tsx:548. Same false claim, different repo; named here rather than left as a fixed-everywhere implication. |
A retry job armed on fable-decide |
exit 2 meant retry, and so did a typo in a flag | exit 2 means retry and carries retry_after; a usage error is exit 64 and never retried; a durable wall is exit 4 and never retried. |
What this does NOT do, stated so nobody assumes it
It does not make the two rungs equivalent. Fable is asked first because it is the better model on these questions; Astra answers only when Fable could not. Nothing here routes to Astra for cost or speed.
It does not give Astra the same reading power. The Astra rung runs
codex exec -s read-only, which can open a repo file a--sourcenames but is not a browser. A question whose sources are all URLs is answered by a rung that read fewer of them than Fable would have. The receipt records the source list, so this is auditable after the fact rather than invisible; making it symmetric is open.It does not watch itself — a canary does.
fable-decideis a CLI invoked on demand: nothing schedules it, so no artifact's silence means "broken", and afile-ageover~/.claude/jobs/fable/would render a healthy quiet day as degraded.relay/tests/systems.test.ts:86refused a catalog entry with"signals": []on exactly that reasoning, and it was right to.So the catalog entry is carried by
decision-ladder-canary(local-bin#110, launchdco.propflow.decision-ladder-canary, 300s): it reads ccswitch's snapshot throughfable-decide's ownpick_seat()and the codex access token'sexpclaim, calls no model, and writesladder OKonly while both rungs are reachable. TheSYSTEMS.mdsignal is alog-match-ageon that literal text at 900s, so the row goes stale precisely when a rung is missing — and DEGRADED/DOWN lines, written every run so a reader sees what is wrong, do not keep it green. The canary exits 0 whatever it finds; "the canary crashed" is launchd's fact and "the ladder is down" is the log's, because those need different people.It does not force anyone to run the ladder.
blocked raisestill accepts a free-text--fablestring with no receipt behind it. That gap predates this ADR and is unchanged.
Alternatives considered
- Leave it to the Operator to notice and redirect. This is the status quo, and the status quo is the ninety minutes. The instruction "if Fable is walled, ask Astra" is a prompt, and this fleet's standing lesson is that a field which can say "I didn't" is a prompt, not a mechanism.
- A separate
astra-decidetool the caller chooses between. Rejected: it puts the routing decision back on a caller who, by construction, is the one that did not notice. It would also fork the two-pass discipline and the class gate across two files, which is how one of them goes stale. - Fall through on every non-zero exit. Rejected — it is exactly the collapse in rule 3, and it would launder refusals.
- Ask BOTH models always and compare. Rejected: it doubles the cost of the common case to buy a cross-vendor agreement check nobody asked for, and a disagreement between two vendors has no defined meaning here — the reversal guard is a claim about one model's stability under reframing, not a vote.
Related
- ADR-0003 — the supervision ladder: rungs integrate by signals, never calls. This ladder is inside one process by design; it is a decision ladder, not a supervision one, and the rungs here are models rather than systems.
- ADR-0004 — a decision that changes how systems relate is written at decision time, by whoever is in the room.
docs/the-four-roles.md— "The Fable decider", updated by the same PR.SYSTEMS.md— thedecision-ladderentry, carried by the canary's signals (the ADR's former open item, closed the same day).