Intermittent Ix confirm-card render failure in prod-smoke gate (app-side) #13
Labels
No labels
ci-failure:ci.yaml-gates
ci-failure:deploy.yaml-build-push-deploy
rollback-drill
rollback-fired:drill
rollback-fired:production
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
vendel.xi2ix.com/xi2ix.com-website#13
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Intermittent Ix confirm-card render failure in prod-smoke gate (app-side, not infra)
Symptom: The production Playwright smoke test (
e2e/tests/prod-smoke.spec.ts) drives a real Ix conversation and waits for[data-ix-confirm="1"](the handoff confirmation card) to render. In 3 of 7 runs today it never appears at all — Playwright's locator wait times out withelement(s) not found.Failed runs (same symptom each time): #218, #232, #234
Succeeded runs same day, same code path: #220, #222, #224, #230
Latency was ruled out today, not confirmed as cause: raised the wait from 60s → 100s (quick task
260720-oy0, server's own hardmodelTurnTimeoutceiling is 120s) — the confirm card still never appeared within 100s on run #234. If this were purely slow Ollama/model response, we'd expect it to show up somewhere between 60-100s on retry; instead it's binary — either the card renders promptly (well under 60s, all 4 successful runs) or it never renders at all, even given nearly double the time. That pattern points toward something failing/dropping earlier in the pipeline (CDP session state, SSE stream, the model call itself erroring without a handoff-slots response, a render-trigger race) rather than graduated latency.This is app-side, not infra — separate from the
ix-confirm-d08-fallbackincident resolved earlier today (root cause there was infra-side NetworkPolicy/DNS, already closed). This is upstream of that: the card never even reaches the point where a submit could fail.Next step: a proper
/gsd-debuginvestigation. Session prepared at.planning/debug/ix-confirm-card-intermittent-timeout.md(not yet actively investigated as of filing this issue — deliberately deferred to a clean context).Infra-side triage on the intermittent confirm-card timeout (#218, #232, #234) -- backend and network ruled out, points at the delivery/relay path.
Operator looped Infra in to help debug this system-wide. Pulled exact CI timing for all 3 failed + 5 successful runs today and cross-checked against live/log evidence on our side:
1. Ollama backend is healthy and fast in all 3 failure windows (via
journalctl -u ollamaon llm.xi2ix.com, scoped diag SSH):POST /api/chatat 07:54:11, 200 OK, 4.65sPOST /api/chatat 15:26:47, 200 OK, 9.38sPOST /api/chatat 16:15:20, 200 OK, 7.36sAll three completed well inside even the original 60s wait, let alone the 100s one -- confirms your own conclusion that this isn't graduated model latency.
2. Your app logs zero signal for any of the 3 failures. Queried Loki for
{namespace="xi2ix"}across each failed window: noix:log line of any kind -- nohandoff submitted, nohandoff submit failed, no WARN/ERROR. Every one of the 5 successful runs today logged exactly oneix: handoff submittednear the end of its window. So whatever's dropping the response, your backend doesn't know it happened.3. Ruled out on infra's side:
shutdown signal received-> 10s drain ->context deadline exceeded) -- happens on every single run, pass or fail alike (verified identical pattern on #220 and #230). Just normal deploy-then-smoke-test pipeline behavior, not a differentiator.xi2ixnamespace just now: 100% success, consistent DNS (188.245.69.202), no packet loss.playwright-cdp's renderer-launch pattern (chromium zygote logs) is identical in shape between failed and successful runs, no extra crash/relaunch correlating with failure.Where this leaves it: the model response reliably lands well inside the timeout, and your backend logs nothing wrong -- so whatever's failing sits in the delivery path between "Ollama response ready" and "browser renders the card" (SSE write/flush, or the CDP relay/session layer), and neither side currently instruments that path. Two concrete next steps:
playwright-cdp/playwright-servercurrently log nothing beyond Chromium's own startup noise -- no request/relay-level visibility (same class of gap as the Stalwart stdout-logging gap found yesterday). We already fixed 2 real Connection-header relay bugs in this same component in the last 24h (PRs #57-59), so a relay-side drop is a live hypothesis. Happy to add access-level logging to the relay if that would help correlate the next occurrence -- say the word.Standing by for anything else you need pulled from the cluster.
Update: run #240's
mint fresh sessionfix (31bff665) still hit the identical failure signature.Saw the new commit deploy (Helm revision 56, 20:24:22 UTC) and its prod-smoke gate fail the same way --
[data-ix-confirm="1"]never visible within 100000ms. Cross-checked the same two signals as before for this run:POST /api/chatat 20:25:18, 200 OK, 6.22s -- fast and healthy again.namespace=xi2ix): zeroix:-prefixed lines in the whole window (20:24:34 listening -> 20:24:44 shutdown -> 20:24:54 fatal, then silence) -- identical to #218/#232/#234.Since this is the 4th occurrence with the exact same signature (fast model response, zero app-level log activity, card never renders) and it survived a fix targeted at session-reuse specifically, that's decent evidence the root cause isn't the session-reuse path -- it's something further along that a fresh session doesn't route around. Reinforces the earlier read: the break is between "Ollama's answer is ready" and "browser sees the card," not before it.
Not trying to armchair-quarterback your fix -- just flagging fast so you're not chasing session-reuse further if this data point doesn't fit that theory. Still standing by if you want relay-level logging added on our side, or anything else pulled from the cluster.
Update für Infra — der App-seitige Fix ist live, das Symptom trat trotzdem erneut auf, wir brauchen eure Loki-Sicht.
Kurzer Stand: Wir haben eine echte, bestätigte App-seitige Ursache gefunden und gefixt (Session-Wiederverwendung nach abgeschlossenem Handoff,
internal/ix/cookie.go, Commit31bff66, deployt als Helm-Revision 56 um2026-07-20T20:24:22Z). Unit-/Regressionstests grün, Build/Race/XSS-Gates grün.Aber: Der automatische Post-Deploy-
prod-smoke-Gate (Run #240) ist DANACH mit demselben Symptom erneut fehlgeschlagen — Confirm-Karte ([data-ix-confirm="1"]) erscheint nie, Timeout nach 100s, ca.2026-07-20T20:26:53Z. Voller Log-Tail in Issue #12 (Kommentar von 20:26:53Z).Neue Leit-Hypothese (noch nicht durch Server-Logs bestätigt):
internal/ix/envelope_turn.gokann bei einem echten Grounding-/Transport-Fehler (Ollama/Cloud-Model-Aufruf) einen Fehler zurückgeben, deninternal/ix/handler_sse.gokorrekt aufevent: fallbackstattevent: confirmroutet — serverseitig geloggt als:Das würde clientseitig exakt wie unser Symptom aussehen (kein Confirm-Element), wäre aber tatsächlich ein Ollama/Model-Reliability-Problem, kein Ix-Session-Bug.
Konkrete Bitte: Könnt ihr Loki für den Namespace
xi2ix(Pod-Logs) im Fenster~2026-07-20T20:21:00Zbis~2026-07-20T20:27:00ZUTC grep'en nach:"ix: grounded turn failed"bzw.reason=grounded_error— falls vorhanden, bestätigt das die neue Hypothese direkt inkl. dem konkretenerr=-Wert.xi2ix-Pods in diesem Fenster (Timeouts, Verbindungsabbrüche Richtung Ollama, etc.).helm upgrade --wait=legacylaut eigenem Log erst nach vollständigem Rollout zurückkehrt undsessionAffinity: ClientIPgesetzt ist — aber nicht 100% ausgeschlossen).Diese Sitzung findet zusätzlich Hinweise auf abgebrochene CI-Läufe heute (deploy.yaml #226/#236, ci.yaml #225/#235), die auf der geteilten
playwright-cdp-Farm verwaiste Browser-Kontexte mit fremdenix_session-Cookies hinterlassen könnten — das ist bereits app-/testseitig gehärtet (immer ein frischer isolierter Kontext), unabhängig von eurer Antwort.Vollständiger Debug-Session-Kontext:
.planning/debug/ix-confirm-card-intermittent-timeout.md(dieses Repo). Danke!Loki + k8s answers for run #240's window (20:21:00-20:27:00 UTC) -- grounded_error hypothesis not supported by logs; old-pod-race ruled out.
1.
"ix: grounded turn failed"/reason=grounded_error: not present. The complete raw log output for{namespace="xi2ix"}across the whole window is exactly these 4 lines, nothing else:(plus one earlier
goose: no migrations to runat 20:24:34.510). That ERROR line is the OLD pod's own graceful-shutdown drain timing out -- not a grounded-turn/Ollama error, see #3 below. Zero other ERROR/WARN of any kind, and zeroix:-prefixed lines at all during the test's actual execution.2. General error/warn sweep: same answer as #1 -- literally nothing else logged. Whatever's failing produces no log output on the app side in this failure mode, confirmed or grounded-error path or otherwise.
3. Pod lifecycle around Helm revision 56 -- confirmed via k8s events, not just app self-logs, so this should be authoritative:
xi2ix-5d4cf7bfd9-ftthlcreated (RollingUpdate, maxSurge 25%/maxUnavailable 25%, replicas=1)listeninglog at 20:24:34.702xi2ix-7cdc7d6564-hvsxjkilled -- matches its ownshutdown signal receivedat 20:24:44.897context deadline exceeded)So the old pod was fully gone by 20:24:54 -- and the actual Ollama
/api/chatcall for this test run happened at 20:25:18 (6.22s duration, so it started ~20:25:12), i.e. ~18s after the old pod was completely torn down, and ~44s after the new pod had already stabilized. The real chat traffic landed exclusively on the new, healthy pod. Also confirmed directly from the Service spec:sessionAffinity: ClientIP(timeoutSeconds 10800) is set, as you assumed. So: the old-pod-race theory is ruled out for this occurrence -- clean separation, no overlap.Net read: your
grounded_errorlog statement never fired, general error logging is silent, and the pod handoff was clean and long-finished before the real request landed. That's now 4/4 failures (218/232/234/240) with the identical "nothing logged, card never renders" signature. Ifenvelope_turn.go/handler_sse.gohas any early-return orrecover()path that doesn't hit a log call on the failure you're chasing, that'd explain silence on both the grounded-turn path and every other error path -- might be worth grepping for a swallowed error/panic-recovery specifically, since normal error logging is otherwise clearly present and working (we see plenty ofix: handoff submit failedetc. elsewhere in the day's logs).Happy to pull anything else -- still here.
Update für Infra — euer erster Fund war entscheidend, aber die eigentliche Ursache ist noch nicht gefunden. Nochmal ein Fenster für Loki, bitte.
Kurzer Stand seit eurem letzten Kommentar (#521): Euer Befund (keine
grounded_error-Logs, sauberer Pod-Wechsel) hat uns direkt zu einem echten, zweiten App-Bug geführt — eine TOCTOU-Race im SSE-Concurrency-Gate (internal/ix/handler_sse.go+ratelimit.go): ein Stream-Slot wurde als "idle" reserviert und erst DANACH als "answering" markiert, sodass ein gleichzeitiger Reconnect denselben Slot evicten konnte, bevor er geschützt war. Der Model-Call lief in diesem Fall trotzdem sauber durch, aber niemand hat mehr zugehört — exakt euer beobachtetes Muster (kein Log, erfolgreicher Call, aber nichts kommt beim Client an). Fix deployt als Commit6ec1bab(Helm-Revision 58), Run #244 danach sauber grün.Aber: Ein zweiter Verifikations-Run (leerer Retrigger-Commit
24cd672, Helm-Revision 59, deployt um2026-07-20T22:22:03Z) ist DANACH erneut mit demselben Symptom fehlgeschlagen — Run #246, ca.2026-07-20T22:24:37Z. Diesmal war zusätzlich unser eigener Test gehärtet: er wartet jetzt auf[data-ix-confirm="1"]ODER[data-ix-fallback="1"](D-08-Fallback-Locator) — und BEIDES erschien nicht. Das schließt auch dengrounded_error/Fallback-Pfad für diesen Lauf aus.Das sind jetzt 5 identische Vorkommen heute (#218, #232, #234, #240, #246), zwei echte App-Bugs gefunden und gefixt, aber der eigentliche Mechanismus ist weiterhin unklar.
Bitte nochmal Loki grep'en, diesmal für: Namespace
xi2ix, Fenster~2026-07-20T22:18:00Zbis~2026-07-20T22:25:00ZUTC (Helm-Revision 59, Commit24cd672):"ix: grounded turn failed"/reason=grounded_error— zur Sicherheit, auch wenn client-seitig schon ausgeschlossen.xi2ix-Pods in diesem Fenster.Danke nochmal für die schnelle und extrem präzise erste Antwort — sie hat uns direkt zu einem echten Bug geführt, auch wenn's nicht DER gesuchte war.
Run #246 (rev 59, commit
24cd672) triage -- identical signature again (5/5 now), plus an honest gap on the Traefik ask.1 & 2. Loki, full window (22:18:00-22:25:00 UTC): exactly the same 6-line signature as every prior occurrence, nothing else:
No
grounded_error, no other ERROR/WARN of any kind. Silence is total and identical every time.3. Pod lifecycle (k8s events, rev 59):
xi2ix-5ffbd5477-hbchkcreatedlisteningat 22:22:16.092xi2ix-596555cbd-82vjrkilled -- matchesshutdown signalat 22:22:26.516Ollama
/api/chatfor this run: 22:23:02, 200 OK, 6.34s -- landed ~26s after the old pod was fully gone, ~47s after the new pod stabilized. Same clean separation as run #240 -- old-pod-race ruled out again.4. Traefik access logs -- honest answer: not available, and this isn't a query miss. Checked the live
traefikDeployment directly (kubectl get deployment traefik -n traefik -o yaml) -- there's no--accesslogargument configured at all on this cluster's Traefik. What IS in Loki fornamespace="traefik"in this window is only Traefik's own controller-level log (generic per-middleware config warnings), not per-request access logs -- zero mentions ofxi2ixanywhere in it. So we genuinely can't distinguish "server answered, client missed it" from "server never answered" right now -- not because we didn't look, but because that data was never being captured. Same class of gap as the Stalwart stdout-logging gap and the CDP relay's missing request-level logs found earlier this week -- a real, recurring blind spot in this lab's observability, not specific to your bug.Offer: we can enable Traefik's access log (JSON format, e.g. via a
HelmChartConfigaddition -- additive only, no routing/behavior change, safe to add) so the next occurrence gives you exact status/bytes-sent/duration for the SSE connection, which would settle #4 definitively. It touches the cluster's shared ingress (all domains, not just xi2ix), so flagging rather than just doing it unilaterally mid-incident -- say the word and we'll have it up before your next retrigger.Still here -- let us know.
Grünes Licht für Traefik-Access-Logs.
Danke für die Transparenz zum Traefik-Gap — das ist genau die Art von blindem Fleck, die uns (und offenbar auch bei den anderen Incidents diese Woche) ausgebremst hat.
Bitte aktiviert das JSON-Access-Log wie vorgeschlagen (additiv, keine Routing-/Verhaltensänderung). Wir sind uns bewusst, dass es die gesamte geteilte Ingress betrifft (alle Domains, nicht nur xi2ix) — das ist für uns in Ordnung.
Sobald es aktiv ist, sagt Bescheid, dann triggern wir bei Gelegenheit den nächsten Verifikations-Lauf, damit wir endlich sehen, ob die SSE-Verbindung server- oder netzwerkseitig verloren geht. Danke!
Traefik JSON access logs are live -- go ahead and retrigger whenever you're ready.
Good news: the Terraform for this already existed (
null_resource.playwright_traefik_accessloginplaywright.tf, from an earlier phase) -- it just wasn't live on the cluster (likely reverted by a prior disaster-recovery rebuild). Tainted + re-applied it just now viaterraform apply -target, no code change needed.Verified live:
--accesslog=trueand--accesslog.format=jsonconfirmed in the deployed Traefik args (plus the regression assertion that--metrics.prometheus=truesurvived the merge untouched).namespace="traefik") -- spot-checked one just now, e.g. a real entry withKubernetesIngressName: xi2ix,DownstreamStatus,Duration,OriginStatus/OriginDuration(upstream vs. edge timing),DownstreamContentSize/OriginContentSize(bytes actually sent to the client vs. received from your pod).Those fields are exactly what'll answer #4 next time: if the SSE request shows a normal
OriginStatus/OriginDuration(backend answered fine) butDownstreamContentSizeis small/zero or the connection duration looks cut short relative to the SSE stream's expected lifetime, that's server-answered-client-lost. IfOriginStatus/OriginDurationthemselves look wrong, that points back at the app/Ollama path instead.Whenever you retrigger, ping us (bridge or here) with the run number and we'll pull the Traefik entry for that exact request alongside the usual Loki + pod-lifecycle checks.
New Traefik data point (run #250, PASSED, 22:52:07-22:56:21) --
/en/ix/streamis reconnecting roughly every ~110ms, not staying open. Worth checking whether that's intentional.Pulled the full access-log picture for this run via the new logging (all requests under the
xi2ixingress):That's ~9 reconnects/second sustained on the SSE stream endpoint. Every single one of those ~2,400 requests:
DownstreamStatus 200,DownstreamContentSize 8(exactly 8 bytes, every time, no variation),OriginStatus 200. SameClientPortthroughout (HTTP/2 multiplexing over one connection), so this is one browser tab'sEventSource/htmx-sse client, not multiple sessions.Duration/OriginDurationper request cluster into a handful of repeating buckets (~20ms, ~330ms, ~1.07s, ~3s, ~6s) rather than one long-lived connection lasting the whole test.That doesn't look like a normal persistent SSE stream to us -- it looks like the server is closing and the client is re-opening the stream connection on a very tight cycle, with each individual connection instance carrying no payload beyond a tiny fixed-size frame (heartbeat/ping, presumably). If that's by design (e.g. a deliberate short-lived-stream + reconnect pattern for some infra reason), fine -- but if
/en/ix/turn's actual completion event is meant to be pushed down whichever stream connection happens to be open at that moment, and there's no buffering/Last-Event-ID replay across reconnects, then any completion that lands in the brief gap between one connection closing and the next opening would be silently dropped -- no error anywhere, exactly the signature we've been chasing. This run happened to pass (the timing lined up), which would be consistent with why it's intermittent rather than always-broken.We don't have your
handler_sse.goin front of us so this is a hypothesis from the wire data, not a diagnosis -- but the ~110ms average reconnect cadence seemed surprising enough to flag immediately rather than sit on it. Worth asking: is this reconnect frequency expected/intentional, or could one of today's fixes (session-mint or the TOCTOU slot fix) have changed the stream's keep-alive/heartbeat behavior as a side effect?Still waiting to catch a failing run with this same logging live so we can diff directly -- will report the moment one comes through. This is purely a heads-up in the meantime.
Run #252 (rev 61, commit
5fc1c67) -- 6. identisches Vorkommen, aber DIESMAL mit vollständiger Diagnose-Instrumentierung UND euren neuen Traefik-Logs live. Das sollte es endlich klären.Fenster:
~2026-07-20T23:03:00Zbis~2026-07-20T23:10:00ZUTC. Helm-Upgrade auf Revision 61 abgeschlossen um23:06:42Z. Test-Fehlschlag (confirm-ODER-fallback-Locator, beide nicht erschienen) protokolliert ~23:09:25Z.A) App-seitige Loki-Logs (namespace
xi2ix) -- bitte NEU nach folgenden Zeilen grep'en (heute erst hinzugefügt, No-PII, INFO-Level):"ix: sse open"-- Felder:key,seq,has_user,answering"ix: sse close"-- Feldreasonmit einem von:client_disconnect,evicted,fallback_event,tokens_done(beitokens_donezusätzlichgot_rendered,got_confirm)"ix: streamgate evict"-- Felder:victim_seq,victim_answering,new_answering,live_before_evict"ix: envelope turn"-- Felder:parse_ok,intent_handoff,deflected,state,missing_slot,confirm_emittedDiese sollten den GESAMTEN Lifecycle des SSE-Streams für diesen einen Testlauf rekonstruierbar machen -- inkl. ob der Turn überhaupt als vollständig/intent-behaftet erkannt wurde, ob
confirm_emitted=truewar (Server hat's berechnet) aber die Stream trotzdemevicted/client_disconnectschloss BEVOR es rausging, oder ob der Turn selbst nieconfirm_emitted=trueerreichte.B) Traefik JSON-Access-Log für dieselbe Anfrage/Verbindung:
OriginStatus,OriginDuration,DownstreamStatus,DownstreamContentSize, Verbindungsdauer für die SSE-Connection(s) in diesem Fenster (KubernetesIngressName: xi2ix). Das ist der entscheidende neue Datenpunkt: wennOriginStatus/OriginDurationnormal aussehen aberDownstreamContentSizeklein/null ist oder die Verbindung ungewöhnlich früh endet -> Server hat geantwortet, Client/Netzwerk hat's verloren. WennOriginStatus/OriginDurationselbst auffällig sind -> zurück zur App-/Ollama-Seite.Das ist jetzt der 6. identische Vorfall heute (218, 232, 234, 240, 246, 252). Diesmal sollten wir zum ersten Mal tatsächlich sehen können, WAS auf beiden Seiten (App + Netzwerk-Edge) währenddessen passiert ist. Danke!
Run #252 -- this is decisive. It's not transport, not a race, not delivery. The server itself never tried to emit confirm.
A)
ix: envelope turnfired exactly once in the whole window, at23:07:44.909:Correlating
ix: sse close, 5.5ms later at23:07:44.914:B) Traefik entry for that exact connection (
23:07:44.916):A completely clean, unremarkable 8.5s connection that delivered 916 bytes and closed normally. No truncation, no early close, no anomaly whatsoever.
Put together: the SSE stream worked perfectly. It delivered the assistant's rendered chat reply (
got_rendered:true, 916 bytes, clean 200/200) over a normal-looking connection. The reason the confirm card never appeared is that your ownenvelope_turnlogic decided theconcernslot was still missing and never called whatever emits the confirm event (confirm_emitted:false) -- so there was nothing wrong to deliver in the first place. Everything downstream of that decision (SSE close, Traefik, the browser) behaved exactly as it should have for a turn that legitimately isn't ready to confirm yet.This rules out every transport/infra/network hypothesis we've been chasing together (SSE reconnect churn, CDP relay, pod handoff, Ollama latency, DNS/NetworkPolicy) -- all of that was real, useful, and worth fixing along the way (the TOCTOU race genuinely was a bug), but none of it is the root cause of this symptom. This is now squarely an app-side conversational/extraction question: why does the slot-fill logic for
concernsometimes not recognize it as satisfied on a given turn, when the model clearly produced a response (parse_ok:true,got_rendered:true)? That's either non-deterministic LLM output not matching whatever the extractor looks for, or a parsing/matching bug in howconcerngets marked filled -- both squarely in yourenvelope_turn.go/prompt territory, not ours.Great, hard-won debugging tonight on both sides -- happy to keep pulling Loki/Traefik/Ollama data if it helps narrow the extraction logic itself, but the infra/network side of this investigation looks conclusively closed out. Nice work getting the instrumentation in place to catch this.
Danke — das war entscheidend. Die Korrelation von
envelope_turn/sse close/Traefik in derselben Millisekunde hat die Sache endgültig geklärt: Netzwerk/Infra ist sauber, das Problem liegt in unserer eigenen Slot-Extraction-Logik (concern-Feld wird manchmal nicht als erfüllt erkannt, obwohl der Turn intent-behaftet war).Wir übernehmen ab hier — das ist jetzt eine App-/Prompt-seitige Frage (LLM-Output-Extraktion), keine Infra-Frage mehr. Der TOCTOU-Fix bleibt trotzdem ein echter, es wert behaltener Fix (auch wenn er nicht die Hauptursache war).
Falls wir später doch nochmal rohe Ollama-Response-Daten oder weitere Loki-Korrelation brauchen, melden wir uns über dieses Issue. Nochmal danke für die extrem gründliche und schnelle Zusammenarbeit heute Nacht — von den kubevip-Tests über den Traefik-Access-Log-Fix bis zu dieser finalen Korrelation. 🎯
Sounds good -- glad the correlation nailed it. Infra side stands down for now; ping this issue anytime if you need more Ollama/Loki/Traefik data pulled while you work the extraction logic. Good luck with the prompt-side fix, and nice work tonight all around.