Back to case studySource

CHITRAGUPTA · FAILURE LOG

Eight things that broke, and the rule each one left behind

Nearly every non-obvious line in this codebase is scar tissue from a specific bug. This is the log kept alongside it — symptom, root cause, and the rule that came out. Two of these are still open, and they are here for the same reason as the rest.

On these quotes. They come from this project's own decision log and from session exports captured while each bug was live, not reconstructed afterwards for this page.
CASE 01Fixed

The camera kept switching itself off

SYMPTOM
The user spent most of a session asking why the camera kept closing. One step — “Prepare tadka” — was marked complete off a note reading “oil poured into an empty pan, no ingredients added yet”.
ROOT CAUSE
One boolean on log_observation was doing three unrelated jobs: it guaranteed a spoken alert, it marked the task item complete, and it closed the camera. It was described to the model as “important enough to guarantee they're told” — so the model set it on ordinary progress notes. It was behaving exactly as documented.
Split into two flags with one effect each, and the completion path guarded so it only fires on a genuine “find X” goal. A stray flag is now logged, spoken, and explained back to the model in the tool result so it corrects itself next turn.
THE RULE IT PRODUCED
One flag, one consequence. If a boolean drives both a user-visible action and a state mutation, split it.
CASE 02Fixed

Frames disappeared, leaving no trace at all

SYMPTOM
Nothing. That is the entire problem — there was no symptom to find.
ROOT CAUSE
A cheap yes/no call ran before the real one: is this frame relevant to the current goal? If it said no, the tick returned empty. On the client that is byte-identical to a legitimate “nothing has changed” silence. A frame showing exactly the thing the user asked for could be discarded by a crude misjudgement and leave no evidence anywhere in the system. It also doubled the vision calls per tick.
Removed entirely, with no replacement. The browser-side diff gate and the model's own silence protocol are the cost controls, and relevance is decided in one documented place rather than two.
THE RULE IT PRODUCED
Never add a pre-filter whose “no” is indistinguishable from a legitimate quiet outcome. Failures must be traceable, or they are not failures — they are behaviour.
CASE 03Fixed

The camera contradicted itself every few seconds

SYMPTOM
Four consecutive captions of the same pan, seconds apart, with the scene barely changing — and the reasoning model logging the contradictions as fact.
"Cumin seeds and chopped red onions are sauteing"
"Chopped onions and red chilies are being sauteed"          ← cumin gone
"The cumin seeds ... have not yet been added"               ← now denied
"Cumin seeds or other spices are visible among the onions"  ← back
ROOT CAUSE
Two causes. First, the prompt named the step's ingredients, which turned the job into a roll-call — and a roll-call over small ambiguous items (cumin among browned onions, at 640 px) produces confident false negatives. “Not yet added” is a claim about a negative that a single frame usually cannot support.
Second, every vision call is independent: no memory, no shared attention. The previous caption was already sitting in a buffer and the vision stage simply never used it. It is now passed back as text, with an instruction to describe change — because a hosted API cannot share attention state across calls, so the comparison baseline has to arrive in words.
THE RULE IT PRODUCED
Absence is only reportable when it is positively visible. “I cannot tell” is a valid answer; a confident false negative is not.
CASE 04Fixed

A blocked search was reported as “nothing found”

SYMPTOM
No web search results found for "…" — returned to the model as authoritative, on exactly the does-this-contain-beef questions where being wrong matters most.
ROOT CAUSE
The search provider serves its bot CAPTCHA with HTTP 202. So the status check passed, the parser found zero results in a CAPTCHA page, and a hard block was rendered to the model as a confident absence. The tool was not failing loudly; it was succeeding at returning nothing.
Now a block raises its own error type, so “blocked” and “genuinely empty” are visibly different results, and a provider chain replaces the single endpoint. Two counter-intuitive findings fell out of measuring it: a spoofed browser user-agent draws more CAPTCHAs than an honest bot one, and the “slow endpoint” was a cold TLS handshake.
THE RULE IT PRODUCED
A failed tool must never render like an empty result. The model cannot tell the difference, and it will treat silence as evidence.
CASE 05Fixed

Correctly shipped work was invisible for an entire session

SYMPTOM
Voice input, the camera toggle and several other features were deployed, verified in the repo, and simply absent in the browser.
ROOT CAUSE
The progressive-web-app service worker is cache-first for the app shell and only re-fetches when its own bytes change. Its cache key sat unchanged for a whole development session, so every browser that had ever loaded the app kept serving the stale shell through every deploy. The code was right; the delivery was frozen.
THE RULE IT PRODUCED
Any change to the shell must bump the cache key. It is a one-line ritual that replaces an entire category of unreproducible bug.
CASE 06Instrumented, unresolved

It forgot everything for three turns, and I could not prove why

SYMPTOM
Thirty-six seconds after receiving detailed instructions, the assistant answered “You'll need to give me a bit more context — what is it that you've managed? Are we working on a recipe, a project…?” Both the conversation memory and the task list had vanished together.
turn context: history_turns=12 (sending 10) task_items=6
              task_list_in_prompt=True prompt_chars=3184
ROOT CAUSE
Ruled out: camera ticks polluting memory, the reply not being recorded, an accidental reset, and the free-tier server sleeping — the gap was four minutes, not fifteen.
The root cause is still unknown, and that is the finding. It could not be diagnosed after the fact because nothing recorded how much context a turn actually carried. “The model ignored the task list” and “the task list never reached the prompt” look identical in a transcript and need opposite repairs. So I shipped the measurement above instead of a guess — one line per turn, on both code paths.
THE RULE IT PRODUCED
When two hypotheses need opposite fixes and the evidence cannot separate them, ship the measurement. A speculative fix here would have added machinery that addressed nothing.
CASE 07Open · top priority

A wrong answer survived being corrected

SYMPTOM
The model found “toor dal” in a plastic bag. The user corrected it — “the thing in the plastic bag is black eyed beans not toor dal”. It apologised, then repeated the claim for four more turns, adding detail it had invented: “on an upper shelf, open and upright, with a black loaf pan behind it”.
ROOT CAUSE
A design gap rather than a bug: nothing is doing the wrong thing, the capability simply does not exist. Observations are append-only. There is no way to retract one, and no way to move a wrongly-completed item back to in progress — so the false note kept riding along in the prompt on every subsequent turn, indistinguishable from a verified one.
It also interacts with case 03: the grounding rule tells the model not to contradict the previous caption about something it can no longer see clearly. That is right for a scene going out of focus and wrong once the user has said the caption was mistaken. Whatever fixes this has to carry an “…unless the user corrected you” clause.
THE RULE IT PRODUCED
A false observation that survives an explicit correction is worse than no observation at all. If a system can write memory, it needs a way to unwrite it.
CASE 08Open

The cost control was a suggestion, and the session died

SYMPTOM
Close-up mode costs 2.4× a normal frame, so it is opt-in per step and the schema explicitly instructs the model to set it back when the close look is done. It never did.
11:26:40   frame detail → fine (live ticks now 1024px)
           …never switches back

           429   198,310 / 200,000 tokens per day
ROOT CAUSE
Its cost was bounded by scope — finishing the step ends it. The step stayed in progress for the rest of the session, so every later frame ran at full resolution until the daily ceiling hit.
The fix is not better wording. It needs a hard bound underneath the soft one: a count of close-up frames per item that reverts automatically, stored on the item so it survives a restart.
THE RULE IT PRODUCED
A cost control that depends on the model's judgement does not hold. Instructions are a hint; the budget needs arithmetic.
09

Most of these were found by reading, not by watching

There is no test suite here, and I would not claim otherwise. Verification is throwaway harnesses plus one artifact that turned out to be worth more than all of them: a full session export.

InstrumentWhat it makes visible
the session exportEvery turn, every tool call, and — after this was fixed — every vision prompt and answer, silent frames included
Groq vision usage:Per-frame image cost. Before this, the number that governs the entire architecture was literally unmeasurable
turn context:How much history and state each turn actually carried into the prompt

The vision round trip was invisible three times over: it never leaves the server as its own request, so it is not a network call anyone can inspect; most camera ticks answer [SILENT], and the client only logged a turn when there was something to render, so the majority of frames left no trace whatsoever; and the one log that did carry it was capped and rolled off. That cap is exactly why case 06 could not be diagnosed — the window had already scrolled past.

Case 03 is invisible in any single turn. Only the sequence shows it — which is the whole argument for logging the thing nobody is looking at.