Projects Daily Briefing

My daily briefing would rather miss a morning than send the same email twice.

An AI-written morning email, why a failed send never retries, and the bugs that passed tests until someone read it.

My daily briefing is built to skip a morning. If the email provider may have taken the message and the system can’t prove it, nothing tries again.

The email is an industry briefing: a lead story, a few takes, some events and something worth reading. A model does the research and writes the sentences. Agents wrote all of the code around it, including the code that checks the model. I wrote none of it.

A sample Briefing edition, opening with its masthead and a Top Story about browsers agreeing on scroll timelines.
A sample edition, rendered through the real template. The publications, stories and events are invented.

Nothing Tries Again

A failed send is normally something you retry. Here a retry is how you get the same email twice.

The provider takes the message, and the function dies before it records the answer. The database says nothing went out. The inbox may disagree. From inside the system there’s no way to tell.

So sending is a one-shot claim per day. Only a prepared edition that has never been attempted can start publishing, and nothing reclaims a crashed or failed attempt automatically. If a later run finds an attempt stuck with an expired lease, it writes down “delivery may be uncertain” and stops. Retries are zero at both the function and the scheduler, and the email carries an idempotency key built from the date.

An expired lease goes to delivery may be uncertain and stops. There is no path from an uncertain delivery back to sending.
prepared
one-shot claim
publishing
An email the provider accepted but the run couldn’t finalize stays in here. It gets no status of its own.
sent
expired lease Retry
delivery may be uncertain
A later run writes this down and stops.
There is no way back.

A morning that really was missed costs little. Only sent editions move the content window forward, so the next edition picks up whatever this one would have covered. A duplicate can’t be taken back.

The provider also reports some failures in its response payload instead of throwing. Without a check for that, a briefing that never left would be recorded as sent.

A duplicate doesn’t have to be the same email. After a successful send, a later bookkeeping failure used to rewrite the row to failed. That would have reopened the content window and covered the same ground the next day: a new date, a new idempotency key, the same news. A finished publication is now protected from that rewrite.

One agent review of the briefing raised 16 findings. Three independent skeptics went at each finding and refuted 14. The duplicate-send hole was one of the two left standing.

The old send rule still exists, switched off. Deleting it while it was live would have left it firing and delivered two briefings, one at the wrong time, because it’s declared retained: removing it from the infrastructure code doesn’t remove it from the world. For the same reason, the schedule that sends every day of the week is named “weekend.” The name is wrong and it stays.

Zero Fetches Is an Error

Give a model a fetch tool and it can decline to use it and answer from memory. The answer reads fine, so the check reads the tool trace instead. A pass whose job is fetching, and that fetches nothing, throws an error.

Events runs search, then fetch, then structure. Those phases got split after a live attempt spent every tool use on search and fetched nothing. The search prose is withheld from the last step, so only the fetch step can supply a fact. The code comment says no phase can “launder a search snippet into evidence.” An agent wrote that comment. I’d like credit for it anyway.

A Check So Strict It Rejected Everything

The obvious guardrail: the model cites a URL, and code checks that the run really touched that URL, byte for byte.

In a live run it rejected all four generated takes against 61 real sources. In a later verification run, 72 searched URLs produced zero exact matches, and the Events section came back empty.

The citation and the trace were spelling the same pages differently. The model cites the canonical page while the trace records what was fetched, and those differ by redirects, tracking parameters and trailing slashes. When it reports a URL it also changes case and adds markdown and trailing punctuation. The check was measuring formatting and calling it truth, and only live runs showed that.

Events discovery now compares a normalized form, and the fetch step only ever receives the trace’s copy of a URL, never the model’s rewrite. It has a limit: a citation on an Event matches by host, which blocks an invented domain but not an invented path on a real one.

Then the same mistake came back. A guard on an event’s reason to go required a number or a named entity. It rejected two real weekend events in a live run and emptied the section. Prose with neither now passes if it’s long enough to be saying something and doesn’t just restate the title.

23 of 24 Is a Rejection

The Top Story scorer has to return exactly one score per candidate, enforced by a schema rebuilt for that day’s slate. 23 of 24 is rejected. So is 24 of 24 plus one it made up. A partial answer would look fine and crown a winner from an unfair comparison.

Failures are classified and retried once, and then the section falls back to research. A complete slate that scores below the bar means a weak news day. The judge is fine, so it doesn’t spend the second attempt.

The model supplies judgments. The weighted score is arithmetic, so code does it.

Research Can’t Make the Send Late

Research and delivery are separate runs. A preparation run starts 90 minutes before the send and freezes the researched sections into the database. The publication run only adds fast sections, renders and sends.

Preparation run
Frozen in the database until the send
Publication run
Adds fast sections, renders, sends
Lead story
Takes
Events
omitted
Reading
A schematic, not to scale. Each bar is a section being researched, and each line is that section waiting in the database.

A test swaps the research builders and the content-window lookup for functions that throw, and it still expects exactly one email. A missing or malformed draft fails loudly instead of starting last-minute research. Preparation may retry, since it just reclaims the same date. Publication may not.

Racing a Timeout Only Stops You Waiting

Race a promise against a timer and the timer wins. The HTTP request carries on anyway, and it can hold the run past its hard ceiling and take finished sections down with it. The deadline now owns an AbortController, and the signal goes all the way down to the SDK call.

Racing a timer
deadline hard ceiling Timer stops waiting HTTP request
Nothing tells the request. It carries on past the ceiling and can take finished sections down with it.
The deadline owns an AbortController
deadline hard ceiling Timer HTTP request signal omitted
The signal goes down to the SDK call and the request ends at the deadline. That section is omitted and the finished ones stay.
A schematic, not to scale.

The timeout setting applies per HTTP attempt, so it isn’t a wall clock. At the SDK defaults, per-attempt timeouts multiplied by retries and continuations gave a worst case of 4,500 seconds against a 900 second limit. Retries on those calls are now zero. The reasoning is written in the code: retrying a long timeout has never once succeeded live, so a transient failure costs one omitted section.

A Label That Outlived Its Data

A retention job erased article bodies after seven days while the row, and its “full content” label, lived on for thirty. A hollow row could collect a ranking bonus and then hand the writer an empty string.

No commit or comment had ever recorded why the purge existed, and it saved almost nothing. The purge is gone, and the bonus is earned from the body itself.

Its neighbor in the same filter layer is my favorite bug on this page. An agent browsing a public source invented listing IDs, and the pages it reached said “Page not found!” The junk filter looked for that phrase without the exclamation mark. So each unique 404 became a story. The filter has heard of exclamation marks now.

For Venue Information, Please Contact Us

Three of the first twelve production runs of Events lost the whole section to timeouts or provider errors, so discovery got demoted. Validated events now live in a ledger that serves them until they happen, and a failed search only costs that day’s new finds.

Every event passes ordered checks in code: URL shape, fetched host, a real calendar date, a real venue, geography and a reason to go that isn’t boilerplate. Each rule keeps its own rejection counter. If you’ve written lint rules for a design system, this will look familiar. Many of the rules trace back to a real bad output from the system this replaced, whose rationale read “Worth a look for the best available fit from today’s event scan” and whose venue read “For venue information, Please contact us.”

The Events work got an adversarial review of its own: 34 findings, 27 refuted, 7 fixed. A date check that only looked at the shape had let June 31 render as “Wed, Jul 1.” The geography check tested city and state separately, so the wrong Gainesville, the one in Georgia, would have passed, since “gainesville” and “GA” were each allowed. The pair has to match now.

One Bad Credential, No Email, No Error

Each section runs isolated. A section that throws is left out, the reason is recorded, and the email still goes.

That also meant one bad credential could empty every section at once. An all-empty run used to return a success. No email, no function error, and a failure alarm looking at a clean run. Graceful degradation could degrade the entire product and report that things went well.

An edition with no substantive content now records “empty” and throws, which raises an alarm.

A misconfigured model client still drops a single section with no error. So the working rule for this code is that tests aren’t acceptance evidence. You render an edition and read the HTML.

Tests Passed. Someone Had to Read It

Two bugs passed tests, types and lint, and only reading a live run caught them. A lead story ended mid-clause. A fallback take printed its own headline twice, because the citation label reused it.

Before
The working group published its final draft on Monday. Two browser engines said they would ship it this year. A third asked for more time to review the sections that…
Fills the budget and stops mid-clause. Passed tests, types and lint.
After
The working group published its final draft on Monday. Two browser engines said they would ship it this year.
Drops the half sentence and leaves the rest of the budget unused.
An invented story. The bar under each card is its length against the same budget.

The truncation fix had a bug of its own, and review caught it before it shipped. Its first version treated any period and a space as the end of a sentence, so it would have cut a story at “U.S. Treasury” with no visual sign that anything was gone. A sentence end now needs a lowercase letter before the period and a capital after it. If one falls past 60 percent of the budget, the truncator stops there rather than show half of the next sentence. Earlier than that, it cuts at a word and shows the ellipsis, which is at least honest.

The citation label became the publication’s host. The regression test asserts the symptom: the headline appears exactly once in the rendered HTML.

A generated UI component gets through tests, types and lint just as easily, and then it’s a green build nobody has looked at.