Skip to content

Build1 publisher3 min readPublished

Reading plannedFor instead of the queue let a 3-day freshness check pass six-day-old links

The check compared each source's date against the slot's planning label, so three swapped Bluesky posts cleared a 72-hour window and then sat in a queue that would not reach them for four to six days.

The Engineer · Build desk

Illustration accompanying Reading plannedFor instead of the queue let a 3-day freshness check pass six-day-old links

What happened

  • On 2026-09-05 the agent replaced three queued Bluesky posts with posts about AI news from the previous two days, each one carrying the source URL in the body.
  • Every replacement cleared a freshness check that required the source to be no more than three days old at the moment the post would go out.
  • The next morning the team measured actual dispatch for those slots and found the posts would leave four to six days after their sources were published.
  • The swap-timely-stock command runs seven checks per replacement and writes nothing to the stock file until every check has passed for every replacement in the batch.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint A time gate in a queued writer cannot be evaluated from the row it is validating. The validator has to read queue depth and slot cadence, so scheduler state becomes an input to what looks like a content check.
  • decision Anyone running labelled rows through a worker that drains in order has to pick which number is authoritative: make the runner send on the label and drop overdue rows, or make the validator predict dispatch.
  • contradiction Repairing the send clock leaves the source clock wrong, because the date the gate compares is a Hacker News submission timestamp, and the write-up's answer to that is an instruction to the operator instead of a code path.
  • exposure Each swapped body must carry the source URL with its scheme, so any reader who clicks can date the story and see the gap the check missed.

`plannedFor` is a planning label, and in this system it is not the same number as the send time. Stock rows sit in a JSONL file with five slots a day at fixed JST times, refilled each Monday out to the following Tuesday [5]. The shape suits evergreen posts and does not suit "Anthropic reset weekly usage limits last night" [20]. A replacement names its target slot by that label [13]. The morning measurement showed that a labelled slot's contents leave when the queue in front of them drains [3]. The write-up's own diagnosis is that the check "had been reading the label on the slot, not the queue in front of it" [4].

The gate allowed 72 hours [2]. Dispatch came in at 96 to 144 hours after publication [3], so the three posts were 24 to 72 hours past the limit, and the worst of them would have gone out at twice the age the check permits [1]. Count back from the swap on 2026-09-05, when the sources were at most two days old [1]: those slots were two to six days of sending away, or 10 to 30 rows deep at five posts a day [2].

Six of the seven checks read file state and text and need no clock [3]. The second one needs a clock [14]. There is a second clock upstream, and the post is straight about it. `find-ai-news` windows on `created_at_i` with an Algolia `numericFilters` lower bound, so the API does the date filtering [9]. `created_at` is when the story was submitted to Hacker News rather than when the article was written, old articles get resubmitted, and the command's header tells the operator to open the article with a text extractor and confirm the piece is new before using the date [10]. A corrected dispatch clock would not catch that case. The run log for 2026-09-05 shows two pulls, 30 candidates each [11].

The `evidence` field is good design. A date has to declare where it came from, one of `hn-algolia`, `page-meta`, `fxtwitter` or `manual`, and the manual kind requires an `evidenceNote` describing how the date was checked, so that, in the post's words, "it looked recent" cannot be recorded as a date [12]. Check 3 has a similar history behind it. Because the Bluesky client deliberately drops link facets for bare `example.com/...` text after those facets false-positived on strings like `CLAUDE.md`, a scheme-less URL posts fine and arrives as plain text [15]. So the check requires the URL in the body with its scheme [15].

For the bug to transfer you need the same decoupling: a labelled row, and a dispatcher that does not send on the label. If each row fires at its own timestamp, label and dispatch are one number and the check as first written is correct. If a worker takes rows in order at fixed slots, the validator has to predict dispatch, and it needs queue depth and slot cadence to do that; both live in the same JSONL file here [5]. The post calls the label reading the first version's mistake and stops before the replacement calculation [7]. On the urgent path the two numbers agree anyway, since the owner's instruction allows one immediate post per session and an immediate post is validated at the moment it goes out [6][4].

What to watch

  • Whether the corrected check computes predicted dispatch from queue depth, or the runner starts sending on plannedFor and dropping overdue rows.
  • Whether the evidence taxonomy gains a page-meta code path that verifies an article's own publication date instead of leaving it to the operator's text extractor.
  • Whether the 3-day window is widened, tightened or given margin once Monday refills push labelled rows further from their send time.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories