Skip to content

Build1 publisher3 min readPublished

A number-gate abort pinned one bad draft at the queue head for 13 days

founder-scout's automated DM lane aborted its whole batch every day because one queued draft contained a revenue figure. The repair records that draft as skipped in the ledger and lets the rest of the batch send.

The Engineer · Build desk

Illustration accompanying A number-gate abort pinned one bad draft at the queue head for 13 days

What happened

  • founder-scout's followers lane last sent a DM on 28 August at 16:35, and its operator found 13 days of zero sends on checking the lane on 10 September.
  • A number-gate called checkColdOutbound went into all seven outbound paths on 28 August to keep drafts from leaking the author's real download counts or revenue figures.
  • As written, the gate called process.exit(3) and halted the entire run if any single draft in the batch contained one of those numbers.
  • The draft that tripped it was addressed to @kouAI_work and contained the phrase "downloads on August 25...", and it was never recorded anywhere.
  • Drafts were still being generated and the send script was still launching every day, and the queue file grew from 336 entries to 390 without a message going out.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint A batch-level abort on a record whose state never changes cannot recover by retrying, because the next run reconstructs the same selection and fails at the same place.
  • decision Anyone shipping a pre-send safety check has to decide whether a violation stops the batch or removes the record. Removing the record requires a durable ledger row and a reason field the next run reads back.
  • exposure Anyone copying this fix takes on a dependency on a string, because the exhausted set is rebuilt by matching 'number-gate' inside the ledger reason. Reword that log text and the blocked record is back in the batch.

A gate that aborts on the first violation depends on something else to move the violating record, and in the followers lane nothing did. The queue file is read from the head, the batch is the first entries off it, and the aborting run exited without recording the draft that caused the abort [8]. So the next day's run rebuilt the same six-entry batch, hit the same blocked draft, and exited 3 again [8].

The queue climbed from 336 entries to 390 while that was going on, 54 new entries across the 13 days, roughly four a day [3][16]. Six drafts were re-examined on every run [8]. The other 384 were never looked at once [17].

The replacement at followers_send.mjs:84-97 runs the same check and does something different with the result. It maps each draft to its handle and its checkColdOutbound hits, keeps the ones with hits, appends a row to the SENT ledger with ok:false and reason 'number-gate: ...', adds the handle to an exhausted set, and builds sendable from the drafts that are not in it [9]. The call to process.exit(3) is deleted, and dm.mjs:82-94 got the same edit [10].

The skip survives to the next run because exhausted is rebuilt from the ledger by matching the string 'number-gate' inside the reason field, at followers_send.mjs:49-51 and dm.mjs:50-52 [11]. That puts the reason text in the control flow. Reword the prefix and the record returns to the head of the queue.

The abort path was not silent in the logs. process.exit(3) was recorded day after day, and it was never wired to a Discord alert, so it never reached anyone [4]. An alert on that exit would have surfaced the fault after the first run on 8/28 [18]. It would not have restarted the lane, because the head of the queue still has to be cleared by a state write.

The second incident from the same day shows how much of this turned on ordering. Composer timeouts for the same three people counted as three consecutive failures, tripped the stop guard, and the four people behind them went unsent for two runs in a row [12]. That one cleared itself, because a timeout is transient. The leaking draft was still in front 13 days after it first blocked a run [2][8]. "An unrecorded anomaly stays parked at the head of the queue, so it never heals itself," bokuwalily wrote [13].

This shape hits a job of yours only when the pre-send check aborts the batch instead of the record, the selection order is stable across runs, and the abort leaves no state behind. A queue with randomised selection, or retries that reorder the head, steps past the bad record eventually. The measurement is now a Japanese comment above the gate in followers_send.mjs: measured 2026-08-28 to 09-10, one kouAI_work draft, 13 days at zero sends [14].

What to watch

  • The reorder of the consecutiveFailures >= 3 guard: the published account of that second fix is cut off, so the ordering change is unverified.
  • Whether the composer-timeout path also gets per-record quarantine, since three transient failures can still stop a run.
  • Whether the 'number-gate' reason prefix stays stable, since the exhausted set is rebuilt by substring match on it.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories