Build1 publisher3 min readPublished
A 7,000-character quality check read a one-line quota message as a short article
For four days in September, note2-daily-stock.sh spent all three of its claude -p passes per run on a body the Max plan limit had already made impossible. The fix is a single grep, run before the file is written.
The Engineer · Build desk
What happened
- From 13 to 16 September the note2-daily-stock.sh generator failed three runs in a row, spending all three of its claude -p passes each time and producing no article.
- When the Max plan limit is reached, claude -p returns a one-line message with no TITLE: line, and the script read it as an ordinary body that fell short of the character minimum.
- The fix greps each pass's raw output for limit wording and breaks immediately on a match, then deletes the partial file and downloaded images, notifies Discord and exits after one call.
- A grep across article-daily-stock.sh, maker-daily-stock.sh and series-daily-stock.sh found no limit-message detection in any of the three scripts that share the same generate-then-retry shape.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- cost The two extra passes are drawn from an allowance that is already spent, so the bill for the misclassification lands on whichever job the schedule reaches after the reset, not on the run that failed.
- constraint Detection by phrase match ties the abort path to the provider's user-facing wording, so quota handling can regress without a line of the script changing.
- decision Copying the pattern into article-daily-stock.sh is not a clean lift, because that script generates posts about quota handling and its own output contains the strings the grep looks for.
- exposure Any of the three unpatched scripts that starts inside a limit window will spend its whole retry budget, plus its backoff sleeps, before it reports a failure no rewrite could have fixed.
The quality check had no way to tell a truncated body from an exhausted quota. `article_quality_ok` asks whether the body clears MIN_CHARS of 7,000 and whether it carries at least two affiliate links [3]. A one-line limit notice fails both tests. The reason handed back to the loop was "quality NG reason: not enough characters" [4]. That is accurate and unusable. At the byte count a truncated generation and an exhausted plan look identical, and only one of them gets better on the next pass [15]. The post's author wrote that a retry loop needs more than the bare fact of failure [12].
The loop already held the evidence. `RAW` comes back from `generate_article_raw` as a string before `write_article_file` touches the disk. The fix reads it there: a case-insensitive `grep -qiE` for limit wording, and a `break` out of the pass loop on a match [5]. When `QUOTA_HIT` is set, the script deletes the half-written output, removes the images it downloaded for that product, posts a Discord notification and exits 1 [7].
Three calls a run across four failing runs is 12 `claude -p` calls that produced no article [13]. The patched path would have spent 4.
What the match tests is the provider's prose. The pattern contains the literal strings "usage limit reached" and "rate limit", plus a reset-time form that matches things like "resets at 3pm" [6]. Reword the notice upstream and the grep stops firing, silently, and the loop goes back to three passes. The sturdier signal is already in the report: the limit message arrives as one line with no `TITLE:` line [2].
That distinction matters more for the siblings than for note2. `article-daily-stock.sh` generates the article series this post belongs to [10]. It writes technical posts about quota handling, and their bodies contain the phrase "usage limit reached" for the same reason this one does. A case-insensitive substring match over a whole generated body would abort on its own subject matter. So before the pattern is copied across, it needs scoping to the first line, or replacing with the shape test.
`run_pass()` in `series-daily-stock.sh` is the closest structural match to what note2 tripped over. Three attempts, each a `timeout 600` on `claude -p`, a WARN on empty output, sleeps of 30 then 60 seconds between attempts [11]. Inside a limit window that is up to 1,890 seconds, about 31 minutes, of a scheduled job waiting on a body the provider will not return [14]. The grep across all three siblings found no detection pattern in any of them [9].
One grep per pass is the cheapest operation in a script that shells out to a language model three times. The post does not report patching the other three.
What to watch
- Whether the three sibling scripts get limit detection added, and whether the match is scoped to the first line of the raw output.
- Any rewording of the Max plan limit message upstream: the pattern matches literal phrases and would stop firing without any change to the script.
- Whether run_pass() in series-daily-stock.sh keeps its 30 and 60 second backoff on empty output once a quota abort path exists.