Repository navigation
Dialog turns terminate as provider_protocol_error after our own validators reject the answer: two session-engine exits bypass the model-output split and requeue to exhaustion #277
Description
Problem
A dialog task failed terminally with event all_attempts_exhausted, reason provider_protocol_error and an empty route object; the signature now has 124 occurrences since 2026-09-09, up from 49 known to the earlier scoped analysis, so roughly 75 new occurrences ran under the deployed model-output/protocol split. The deployed runtime revision equals the repository head merged earlier the same day, so the newest occurrence ran the current code. Verified in the deployed tree: two session-engine exits hard-code the protocol-error class for outcomes where the provider answered and our own validation rejected or emptied the answer (sanitizer collapse to empty, and outbound deliverability rejection); the job-retry handler requeues the whole turn per such reason and, on exhaustion, emits the exact terminal event observed, repeating that reason with no provider or model ids. The routed walker treats the two classes differently, so these sites also charge the provider circuit breaker and walk to a weaker fallback instead of taking the intended same-model re-sample. However, no per-attempt rows or error strings are retained for the newest occurrence, so a genuine endpoint protocol fault producing the identical signature is not excluded; the funnel is verified as possible and reproducible, not yet attributed as the cause of the observed occurrences.
External cause: possible. Code defect: possible.
Observations
- Newest occurrence today 16:41:44-48 UTC: the dialog task on the dialog queue reached state failed after about 4 seconds of processing following about 21 seconds of queue wait; the snapshot records terminal event all_attempts_exhausted with reason provider_protocol_error, workflow dialog and an empty route object.
- The signature has 124 occurrences from first seen 2026-09-09 to last seen today; the earlier scoped analysis retained by the controller knew 49 occurrences through 2026-09-30, so at least 75 new occurrences ran after the model-output/protocol reclassification was deployed.
- The deployed runtime revision equals the repository head, a merge committed today about 12:44 UTC, with restart_count 0 and no OOM kill; the failure at 16:41 UTC therefore ran the current code, including every dialog merge up to and including the newest sampling change.
- Verified in the deployed tree (session engine): when the final answer is empty, becomes empty after response sanitization, or skips an explicitly requested image tool, and when the outbound deliverability validator rejects the answer, the turn reports FailureReason::ProviderProtocolError to the job-retry handler; in all these cases the provider answered and our own validation rejected what it wrote.
- Verified in the deployed tree (job-retry handler): every retryable reason requeues the whole turn on the same queue up to the configured attempt cap or wall-clock budget; on exhaustion it emits the all_attempts_exhausted routing event whose detail repeats that reason with provider_id and model_id unset, which matches the incident snapshot's empty route object exactly.
- Verified in the deployed tree (routed walker): a protocol-class failure charges the provider circuit breaker and walks to the next candidate, while ModelOutputRejected gets same-model re-sampling and never charges the breaker; only model-output rejections take the repair path.
- Verified in the deployed tree (classifier): adapter-level validator rejections such as 'empty final text', 'only protocol artifacts' and leak/pathology verdicts map to ModelOutputRejected, while decode failures such as 'decode chat completion payload' and 'response body is empty' map to ProviderProtocolError; the two session-engine exits bypass this split entirely.
- One adapter family still types its own 'empty final text' rejection as ProviderProtocolError while the shared message-rule classifier maps the same phrase to ModelOutputRejected, so the two layers disagree on the identical condition.
- A merged fix's own description documents live runs where a turn was judged empty and the job was replayed five times with the same result before exhausting; that change capped the forced-tool replays but left the failure class unchanged.
- Route-attempt evidence for the newest occurrence returns no matching evidence, and no per-attempt error strings or raw bodies are retained in scope, so the observed reason cannot be attributed from evidence alone to a decode failure versus a sanitizer or deliverability rejection.
- Queue counters show 30251 completed versus 66 failed dialog tasks (about 0.2 percent) with 3 processing and 2 pending; host, runtime and database evidence is healthy (zero restarts, no OOM, ample memory and disk, zero waiting locks, normal pool and connections), ruling out local resource starvation.
- Existing tests assert the terminal ledger reason codes (sanitized_empty_exhausted, undeliverable_answer_exhausted) for these funnels, but no test asserts the routing event's last_retryable_reason for either funnel, so the incident-facing classification is unguarded.
Hypotheses
- Misclassification funnel (our code): the model's answer was emptied by our sanitizer or rejected by our outbound validation, the session engine typed it as a protocol error, the turn requeued to the attempt cap, and the terminal event repeated the protocol-class reason; the code path to the observed signature is verified end-to-end in the deployed tree without any provider fault.
- Genuine endpoint protocol fault (external): the endpoint returned empty or non-decodable bodies for these requests, the classifier correctly typed protocol errors, attempts hopped and charged breakers, and the chain or job budget exhausted with the identical terminal signature.
- Both funnels coexist and alternate across the 124 occurrences; the four-second final processing window fits both an instant validator rejection after a fast answer and a fast endpoint failure.
Supporting
- The exact terminal signature (event type, workflow, empty route, protocol-class reason) is reproducible in the deployed tree from the two session-engine exits through the job-retry handler, so a complete code path to the observed event exists without any provider misbehavior.
- The two exits contradict the split's own documented policy: when the provider answered and our validators rejected the output, the intended class is the model-output one with same-model re-sampling, not a protocol-class provider failure that charges the breaker and walks to a weaker fallback.
- The recurrence continued (49 to 124 occurrences) under a deployed head in which the classifier and these two exits are unchanged since the reclassification merge, and the newest occurrence provably ran the current build.
- A merged fix's description documents the empty-answer replay-to-exhaustion funnel happening live on the self-hosted dialog hop, confirming the funnel is exercised in production, not merely theoretical.
- Healthy host, runtime and database evidence plus the overwhelming dialog success rate rule out local resource exhaustion, crash loops and lock or pool starvation.
Contradicting
- A pure our-code story is not established: without the newest occurrence's per-attempt error strings or retry-stage job events, a genuine endpoint protocol fault that the classifier correctly reports cannot be excluded.
- A pure external-endpoint story is not established: the verified validator-rejection funnel produces the identical signature without any provider fault, and nothing in the retained scope evidences an endpoint fault specifically.
- The classifier inconsistency on 'empty final text' between one adapter family and the shared rules means even a correct-looking reason string does not by itself identify the funnel.
Related changes
- The reclassification merge Re-sample a rejected model before falling back to one without tools #151 (merged 2026-09-17, in the deployed head) split model-output rejections from protocol errors and added walker-level same-model re-sampling; it migrated the adapter sites but not the two session-engine validator exits, which still emit the protocol class.
- Merge Stop the scaffolding contagion at the engine, and tell a re-sample what was wrong #153 extended the model-output repair policy by carrying a plain-language verdict back to the re-sample; the two unmigrated exits cannot benefit from it.
- Merge Stop tool loops and slow thread history reads, keep memory alive through embedder bursts, route summaries to GPU2 #247 (2026-10-12) documents and caps the live empty-answer replay storm on the primary self-hosted dialog hop, reducing forced-tool replays, but leaves the failure class and the requeue-to-exhaustion behavior for empty answers unchanged.
- Merge Sample Gemma dialog turns as the model card recommends #262 (2026-10-14) changed dialog route sampling parameters; it is the newest dialog-behavior change before the continued recurrence and has no causal evidence either way.
- Merge Route memory workflows through independent cascades #98 introduced the multi-hop dialog cascade and wall-clock budget whose tail reachability these terminal events question.
- Merge Audit LLM tool parser parity #30 audited the response-decoding surface that produces genuine protocol-class failures, sharpening the split the two exits bypass.
- Merge Coordinate incident feedback, quota recovery, and actionable notifications #141 owns the incident-feedback and notification surface these terminal events flow into.
Missing
- The per-attempt error strings or retry-stage job events for the newest occurrence: the event's error field distinguishes the sites ('dialog answer became empty after sanitization', 'dialog answer rejected by outbound validation: ...', 'dialog provider returned no answer...') from a decode-failure string, which would attribute the observed reason to one funnel.
- Retained routing attempt rows for the newest occurrence (the scoped attempt query returns no matching evidence), so the set of providers and models actually tried is unknown.
- Raw provider response bodies behind the occurrences; retention exists but no bodies are in scope.
- A design decision for the deep stage: whether the raw-empty branch belongs to the model-output class (the shared rules map 'empty final text' there) or the protocol class (one adapter family types it there), and whether the two validator exits should reuse ModelOutputRejected or a validator-specific reason.
- Live database values of the dialog workflow's hop budget, wall clock, job-attempt cap and route role/ordering/weight fields at failure time, needed to judge whether the fallback tail was reachable.
- Whether the operator-facing admin report should distinguish validator-exhaustion terminal events from endpoint protocol faults, so operators stop seeing healthy providers as faulty.
Acceptance
- A regression test where a step provider answers with text that sanitizes to empty must end with a model-output-class (or validator-specific) last_retryable_reason in the job-retry terminal routing event, not provider_protocol_error.
- A regression test where the outbound deliverability validator rejects the answer must produce the same reclassification.
- A regression test where the endpoint returns an empty or non-decodable body must still classify provider_protocol_error, preserving genuine protocol-fault semantics.
- Deep-stage verification must pull the newest occurrence's retry-stage job events or error strings and attribute the observed reason to one of the two funnels before any patch is declared causal for the observed occurrences.
Impact
124 captured terminal events.
Exact infrastructure identities and operational evidence remain in the private incident context available to the repair worker.
Activity
iamwavecut commentedon Oct 25, 2026
Problem
A dialog task on the dialog worker queue reached terminal state failed after about two minutes of processing (about five seconds of queue wait), emitting the routing event all_attempts_exhausted for the dialog workflow with an empty route object; the signature has 46 recorded occurrences since mid-September and the sampled task is the newest. For this occurrence the scoped attempt capture retains two attempt_failed rows, both on the primary self-hosted dialog hop, with no failure reason on either row; a prior scoped capture of the same signature showed three rows covering every chain route exactly once. Both readings stay open and are code-verified at the pinned (deployed = HEAD) revision: (a) our own session-engine validator exits (empty-after-sanitization / raw-empty / skipped image tool, and outbound deliverability rejection) type the failure as a provider protocol error, which charges the provider circuit breaker, walks to the next candidate, and requeues the whole turn to the attempt cap, ending in the observed terminal event with provider and model unset — even though the split's own policy says a rejected model output leaves the breaker alone and is re-sampled on the same model; (b) a genuine chain-wide endpoint fault in the window produces the identical terminal signature. No retained reason string discriminates the two for this occurrence, so the deep stage must pull retry-stage job events or error strings before any patch is declared causal.
External cause: possible. Code defect: possible.
Observations
Hypotheses
Supporting
Contradicting
Related changes
Missing
Acceptance
Impact
46 captured terminal events.
Exact infrastructure identities and operational evidence remain in the private incident context available to the repair worker.
iamwavecut commentedon Oct 28, 2026
Problem
A dialog task on the dialog queue failed terminally with event all_attempts_exhausted, reason provider_protocol_error and an empty route object; the incident signature has 127 recorded occurrences. Independently re-verified in the repository at the deployed head (identical to base revision): two session-engine exits hard-code the protocol-error class for outcomes where the provider answered and our own validation rejected or emptied the answer (sanitizer collapse to empty at the raw-empty branch, outbound deliverability rejection), the job-retry handler requeues the whole turn per attempt and on exhaustion emits the observed terminal event with provider and model unset while repeating the protocol-class reason, and the shared message-rule classifier maps the same validator conditions to the model-output class, so the two exits bypass the split. A genuine endpoint protocol fault is not excluded: per-attempt error strings and routing attempt rows for the newest occurrence are not retained in scope, and the classifier's decode-failure path produces the identical terminal signature. Deep stage must attribute the observed reason to one funnel before any patch is declared causal.
External cause: possible. Code defect: possible.
Observations
Hypotheses
Supporting
Contradicting
Related changes
Missing
Acceptance
Impact
127 captured terminal events.
Exact infrastructure identities and operational evidence remain in the private incident context available to the repair worker.