A field note from the AIdeazz AI Lab — a real incident on a live production system, written up from the logs. September 1, 2026.
An agent had been writing the same three sentences into every record for eleven days and calling it a draft. The repair was not a better prompt. It was teaching the model to decline -- and the first thing it did after shipping was refuse to write anything at all.
What it looked like from outside
An autonomous job-discovery pipeline attaches a paste-ready cover letter to every record it creates. For eleven days it produced one hundred and eighty-five of them, and the letter was identical in all one hundred and eighty-five -- the same three sentences with the role and company substituted, and a literal instruction to the operator still sitting in the body telling her to edit the stub and add proof points. Nothing errored. The pipeline reported success on every record. The defect was invisible in the logs because the log measured whether a letter was attached, never whether it was worth sending, so the effect was not a broken feature but a feature that quietly did nothing: applying stayed a manual writing job and only happened on the days someone hand-wrote a letter.
What was actually happening
Two failures stacked. The first was ordinary -- a real tailoring component existed in the Python side of the codebase and had zero callers and zero output files, so the boilerplate path was the only path that ever ran. The second is the interesting one, and it only appeared once tailoring was wired in. The obvious repair is to hand the model the job title, the company and a set of verified facts and ask it to write. That produces confident, fluent, entirely generic prose -- which is the stub with extra steps and considerably harder to spot, because it reads like a real letter. A language model asked to write with insufficient grounding does not decline. It fills the space. The absence of input is invisible in the output.
The fix
Wired the drafting into the single function every discovery source already passes through, so all three ingest paths gained it without touching the pipeline that scores and routes. Then added the part that matters: the component refuses. It fetches the posting text first, prefers the boards' own documented public JSON where one exists, and if it recovers less than a floor of characters it returns an empty letter and the caller keeps the existing stub. It is given a fixed block of verified facts and told that is the only source, because a letter claiming unearned experience is a lie sent under a real person's name. And the result reports whether it actually tailored and which provider answered, so a quiet drop back to boilerplate is visible in the record rather than discovered months later in the tone of the applications. The stub is the floor and never the ceiling: every failure path -- no posting, every provider down, a refusal, a placeholder detected in the output -- degrades to the old behaviour and can never block the record being written.
How I know it worked
Confirmed by running it against a live posting on a board that blocks automated fetching, which is the exact condition the guard exists for. First attempt with the URL alone returned tailored false, zero characters recovered, and a reason naming the cause -- the component declined rather than writing from the title. Second attempt supplied the posting text through the documented fallback parameter and returned tailored true at one thousand three hundred and sixty-three characters, with a log line reading that the second provider answered after one failure. That single failure was the primary model, whose credits have been exhausted since the middle of the month, so the five-provider chain absorbed a real outage inside one request and the record shows which model actually wrote the text. Separately, the boundary between the old and new behaviour is visible in the data: one record created one hour and fifty-five minutes before the fix shipped still carries the boilerplate, and every record after it carries a tailored letter.
The rule this earned
When a model is a component in a pipeline, the expensive failure is not the error you catch -- it is the plausible output produced from nothing. So give the component the ability to decline, make declining cheap, and make the decline visible in the artifact rather than only in a log. Judge the fix by what it refuses, not by what it generates: the first correct behaviour of this one was to produce nothing and say why. And measure the thing the work is for. A log line confirming a letter was attached measured attachment; nobody was measuring whether the letter was worth sending, and that gap ran for eleven days behind a green pipeline.
The named concepts behind it
Naming a failure mode is what makes it possible to recognise the same shape somewhere new, before it costs another weekend.
Fabrication under degradation
When the good model runs out, the fallback does not go quiet — it goes confident.
Every serious AI pipeline has a fallback chain, and the chain is right: when one provider fails, another answers, and the work continues. That is redundancy doing its job.
But redundancy protects availability, not truth. A retry that returns text has succeeded by every measure the system knows how to take. It ran, it returned, it was well-formed, it was the right length. Nothing in that check asks whether the text is true.
This is what makes the failure mode dangerous. A degraded model asked to write about a system it cannot inspect does not stop and say "I do not have this detail". It fills the gap with the most statistically ordinary answer — the thing that architecture usually uses. Asked about checkpointing, it reaches for Redis, because most checkpointing articles involve Redis. The output is fluent, technically plausible, internally consistent, and describes infrastructure that does not exist.
Compare it to a silent failure, which produces nothing and tells nobody. This produces something, and that something is worse, because it passes every automated check and every casual human read. Volume makes it worse still: a pipeline on a schedule does not fabricate once, it fabricates on a cadence, and each copy looks as reasonable as the last.
The defences are structural, not editorial:
- Never let a model re-tell a fact it cannot verify. Assemble published claims deterministically from fields that were measured. A template that interpolates a verified number cannot invent a different one.
- Name the writer in the artefact. If the output records which provider produced it, "everything since June was written by the fallback" is a query rather than an archaeology project.
- Treat a provider downgrade as an editorial event, not just an ops event. Credit exhaustion silently changes who is speaking in your name. That deserves an alert, not a log line.
- Cap the blast radius. Anything published automatically, under a real person's name, on a public surface, should require a verified source — or require a human before it goes out.
The reputational asymmetry is the part worth internalising. A crash costs you an afternoon. Published fabrication costs you the credibility of everything true you ever wrote next to it — and it is discovered by the reader, not by you.
Silent failure
The system did something reasonable, and told nobody.
The most expensive bug class there is, because the clock keeps running while everyone assumes things are fine.
A silent failure is not a crash. A crash is loud and gets fixed. A silent failure is a component making a defensible local decision -- drop this message, skip this record, return an empty string -- that nobody downstream is told about. From the outside, a system that is working perfectly and a system that is completely dead can produce the identical observation: nothing happened.
The defence is not "add more logging". It is to make the healthy state provable, so that "nothing happened" can be distinguished from "nothing was supposed to happen". Two things do that:
- Log the outcome, not the attempt. "sending notification" tells you nothing. "notification DELIVERED (id 4661)" versus "notification REJECTED 400" tells you everything.
- Run a canary. A synthetic transaction pushed through the real path on a schedule, which shouts when it does not come out the far end. Without one, you are relying on a customer to report your outage.
Verify from logs, not config
Configuration tells you what somebody intended. Logs tell you what happened.
A setting, an environment variable or a present API key is a statement of intent. It is evidence that somebody meant for a behaviour to occur. It is not evidence that the behaviour occurs.
The gap between the two is where the longest outages live, because reading the configuration feels like verification. It produces confident, wrong statements: the key is set, so the provider works; the schedule says every fifteen minutes, so it runs every fifteen minutes; the file was deployed, so the new code is running.
Each of those has a cheap, decisive check that costs seconds:
- Probe the dependency, do not read its credential. A key that exists proves nothing about the balance behind it.
- Grep for the action line, not the setup line. A startup banner proves the process started, not that it ever did its work.
- Compare timestamps after a deploy. If the running process is older than the file on disk, it is still executing the previous version from memory.
The rule this earns: never report a system's behaviour from its configuration. Grep the line that proves the behaviour happened, and quote it.
---
This note is one entry in a running wiki of production engineering lessons — every concept linked to the incident that taught it — at aideazz.xyz/ai-ops-wiki.html.
No customer data, credentials, hostnames or internal record identifiers appear in these write-ups.