A PR That Verified Zero Missing Rows Stopped Analysis for Three Hours With One Comment Line

The PR for regenerating 260,000 summaries accounted for locks, rolling deploys and every stray row. One format placeholder inside a comment in the last commit broke the SQL, and all four tests still passed

I shipped a change to regenerate all 260,000 summaries in my disclosure-summary service. The PR counted every row to prove nothing would be missed and even measured migration locks. Right after the deploy, not a single new disclosure got a summary. The cause was one format placeholder Claude had put inside an SQL comment. This episode is about an outage the AI caused and the AI fixed.

What I was trying to do

The day before, I had moved the summary model from haiku to sonnet. Cross-checking 1,360 supply-contract summaries against financial data showed 9.2% of them shifting the amount by a digit — the error crept in while converting won into hundred-millions of won.

Upgrading the model only changes disclosures that come in from now on. So I decided to regenerate the existing summaries too: 261,404 of them.

How I asked for it

The PR was careful. Codex review kept raising P1 issues, and each got fixed.

Right before merging, I asked one thing: were the rows not yet redone with the new model being processed newest first? They weren’t. Reanalysis enqueued the newest disclosures first, but the query that claims work picked the oldest first. I said “fix the ordering too and push it now”, and a commit splitting the sort key per work pool went on last. Deployed at 12:56.

Where it got stuck

The SQL was assembled in Go with Sprintf. A comment explaining the sort keys contained the placeholder %[4]s, and the value substituted there spanned two lines.

-- ── the last two keys differ per pool (CASE WHEN status = 'PENDING' THEN created_at ...,
        rcept_no). ────────      ← SQL from here on

A -- comment ends at the end of the line. The second line escaped the comment and became SQL, and the leftover closing parenthesis broke the syntax. The claim query failed every minute. Disclosures kept coming in, but no summaries were attached, and the home page only shows disclosures that have a summary. So the site looked frozen.

All four tests passed. They only checked whether certain strings appeared in the generated SQL. The broken SQL contained every one of them.

There was a signal 35 minutes after the deploy. While I was asking about something else, Claude checked whether the new prompt had taken effect. The answer was “0 so far, 6 PENDING”, and the last completion time was right there in the table. Claude read it as “not done yet” and moved on. It looked at what was coming in, not at what was going out.

How it got solved

Two hours later, I noticed first.

Why does it look like no new disclosures have shown up since 11:36? Can you check?

The first query was wrong too — it converted the time zone backwards, so it looked like ingestion had stopped. Once corrected, ingestion was fine; analysis had simply completed nothing for 3 hours 24 minutes, with 88 PENDING piled up. Following the claim failed log led to syntax error at or near ".". That was the first time Claude said, “My PR is the cause.”

The stumbling continued during the fix. A verification command wrapped in double quotes let the shell eat $1–$7. A command to revert local changes also wiped the fix it had just made, so it was redone.

The fix itself was simple: take the placeholder out of the comment and describe it in words. Four variants of the query were run through PREPARE on a real Postgres 16 to confirm the syntax. A regression test was added that strips comment lines and checks that the parentheses balance — the broken version has one extra closing parenthesis and fails it. Merged at 15:49; a few minutes later PENDING was dropping from 88 to 58.

Lessons