DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path scoping-notes/loomworks-b96-converse-failure-vocabulary-scoping-note-v0_1.md

B-96 scoping — the converse path's failure vocabulary — v0.1

Date. 2026-08-14 · Item. [B-96] (build list v0.74) — "Couldn't reach the Companion" renders for servers that were reached. Mode. READ-ONLY. Nothing built. Surface 00ed2f2, engine f011c80 (both = origin/main, clean). Ordered questions. (1) Enumerate what the converse path returns on failure and which cases render as "couldn't reach"; (2) can the two sites share one helper without becoming the fenced-against mechanism; (3) is there any case where the sentence is actually true.


0. A correction first, so it does not stand

The B-95 scoping note and the B-96 entry gave 503 no_credential as the in-situ example. That was wrong, and this note supersedes it. POST /operator/converse cannot 503 on a missing credential: the route pre-resolves the key and routes around absence with a composed 200 (_NO_LLM_KEY_FALLBACK, converse.py:590/1164). The 503 lives on adjacent routes (summarize — the CR-171 test pins it) that these two catch sites never call. The class claim survives — a reached-and-answered server rendered as unreachable — but its true members on this route are the 500 and the 422 below, not the 503. The B-96 entry is corrected in build list v0.75 alongside this note; the ranking rationale (main artery, specifically false assertion) stands on the corrected members.

1. The failure vocabulary — five cases, three currently lied about

Both sites (useConversation.ts:448, ChatView.tsx:535 — confirmed: both wrap the SAME route, api<ConverseResponse>("/operator/converse")) catch everything non-401 into the one sentence. What can actually arrive:

| # | Failure | What comes back | Server reached? | "Couldn't reach" true? | |---|---------|-----------------|-----------------|------------------------| | 1 | Thrown fetch (network down, frontend down) | nothing — TypeError, not ApiError | NO | TRUE | | 2 | Engine down/restarting | 502 from the Next rewrite (next.config.ts:29 proxies /api/*127.0.0.1:8000), HTML/text body | the front door only — the Companion's process was NOT reached | TRUE in substance | | 3 | Engine unhandled exception (the red-on-red/B-83 family — a real bug) | 500 {"detail":"Internal Server Error"} — no cause, no stable code | YES — reached and crashed | FALSE — a bug rendered as network weather, retried into | | 4 | Malformed request body (a surface bug) | 422 FastAPI field-error array — precise, names the field | YES — reached and answered precisely | FALSE — a surface defect hidden as weather | | 5 | Session invalid/expired | 401 | — | handled separately today (silent return / api() redirect) — out of B-96's scope |

A finding in the other direction, worth recording: the converse route is deliberately ARMORED. Credit exhaustion, missing LLM key, provider rate limits, rejected keys, ambiguous engagement handles, phase-31 creation errors — all are caught server-side and returned as 200s with composed Companion prose (_operator_message_for_phase31_error, _NO_LLM_KEY_FALLBACK, _companion_error, the exhaustion dialog). The residue that reaches these catch blocks is therefore small and enumerable — the five rows above — which is exactly what makes a precise fix cheap.

2. What the fix branches on — status class, not codes; NO prerequisite

Unlike CR-2026-210 (where not_converged is a stable code to branch on), the converse residue carries no stable cause codes: the 500's body is a constant with no information, the 422 is a field array, the 502 is proxy HTML. But this fix never needed causes — it needs to stop asserting unreachability when the status proves reachability. Branching:

No server change, no prose parsing, no prerequisite. The CR is surface-only.

3. The helper — shareable without becoming the mechanism

One function, converseFailureMessage(err: unknown): string — takes the caught error, returns the sentence per the three branches above. Judgment against the fence: this is not the general mechanism. It is scoped to one route's failure vocabulary (both sites call the same endpoint), returns a string (no component, no registry, no code→message table beyond the three branches), and is useful to exactly two callers. It would START to look like the fenced mechanism if it grew a per-code mapping table or took a route parameter — the CR draft names that as the line, and if a third route ever wants it, that is a new ruling, not an import. Verdict: share it.

4. The true-branch question, answered

Yes — the sentence has true branches: rows 1 and 2 (thrown fetch; 502 engine-down). So it is NOT the passkey sentence's inverse (which had one true case and zero true renders). It is the other failure shape: a sentence with genuine true cases that also renders in the false ones — currently true sometimes, false sometimes, indistinguishable to the reader. The fix's effect is to make it true ALWAYS by confining it to rows 1–2. (Practical note: row 2 — engine down with frontend up — is a real operational state on this deployment: the engine restarts on deploys while the LaunchAgent-served frontend keeps answering. The sentence earns its keep there.)


What awaits ruling

The CR-2026-211 draft (accompanying this note): both sites, the shared helper, the three-branch discipline, the correction folded into build list v0.75. CR-2026-210 holds behind it per the ranking.


DUNIN7 — Done In Seven LLC — Miami, Florida — B-96 scoping note — v0.1 — 2026-08-14 Five failure shapes; three lied about; the armor upstream makes the residue small. The sentence keeps its true branches and loses its false ones.