Skip to content

Let a keyword that failed on infrastructure come back - #216

Merged
ralyodio merged 5 commits into
masterfrom
autoblog-keyword-quality
Aug 28, 2026
Merged

Let a keyword that failed on infrastructure come back#216
ralyodio merged 5 commits into
masterfrom
autoblog-keyword-quality

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The 09:00 failures were an OpenAI 429 — "You have no credits remaining" — found in the Railway logs of the deployment live at the time. 308 keywords were marked failed for a reason that had nothing to do with their content.

REQUEUE=1 emits the update that returns the survivors to the queue instead of the delete that removes bad rows. Survivors only: of the 308, just 44 pass today's gate, because most predate it. Requeuing all would have republished the spam this change set exists to stop, so the same gate that decides what may be created decides what may come back.

Applied to production: 44 requeued.

🤖 Generated with Claude Code

https://claude.ai/code/session_013cbcXdpLFUgJZuVfCB4Gzv

ralyodio and others added 5 commits August 28, 2026 16:07
…n article

The anchored gate shipped and the hourly cron wrote its first 162 keywords
across 11 sites. The vendor and junk classes are gone. Reading the actual
output found two things the fixtures could not have.

**Partial matches leaked.** Eight keywords, all one shape: one generic word of
a multi-word subject matched, and a word from the generic commercial
vocabulary rescued it. "open standards" matched on "open", "software" did the
rest, and logicsrc was queued to write about OBS Studio. "supply chain
security" matched on "supply" and admitted "industrial automation supply".
"online shopping" matched on "online" and admitted "quickbooks online
pricing". So a partial match on a multi-word subject may now only be rescued
by an anchor the site itself supplied — never by the defaults, which are
generic by construction. A complete match is unaffected, including a
single-word subject, where "iptv alternatives" is a real query.

**And the cross-product floor was publishing constructions.** That one is mine
and it was new. Used as per-subject filler it produced "saving money pricing",
"deals platform", "coordination d0rz" and "ai content loop" — on-niche,
gate-passing, and not topics anybody searches. bl0ggers' niche is
"human-in-the-loop AI publishing", so its derived modifiers are literally
"human" and "loop", and crossing a subject with those yields nonsense.

The crosses' real value was always upstream: as DataForSEO seeds they are what
turns "peptide" into "peptide merchant account", and they still seed every
expansion. They are simply no longer published on the strength of being
grammatically adjacent to the niche. They remain the floor, but site-level
rather than per-subject — only a run that would otherwise insert nothing falls
back to them, which keeps "a blog with every upstream down still publishes"
without letting constructions pad a healthy run. The result reports
usedCrossFloor so a site sitting on that for weeks is visible.

All eight leaked keywords are now regression cases, with the on-niche
keywords from the same run that must still pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013cbcXdpLFUgJZuVfCB4Gzv
Companion to purge-offniche-keywords.ts, for rows written before the gate was
tightened. Two verdicts, both computed with the real planner rather than
approximated in SQL.

The second one needed care. A construction cannot be found by checking for a
null search volume, which was the obvious approach: the buyer-journey model
also returns keywords with no volume, and those are the best output in the run
— "zeek vs suricata for network security monitoring" and "langchain
alternatives for agents" both sit in the same null bucket as "deals platform".
So a construction is identified by fingerprint against the cross set the
planner would build for that site, which is exact rather than heuristic.

Ran against the 162 rows from the first production cron: kept 53, removed 95
constructions and 14 partial-match leaks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013cbcXdpLFUgJZuVfCB4Gzv
Publishing stopped on 2026-08-19 and nobody could tell why. All 308 failed
lx_keyword rows carry a null status_reason, because failKeyword() wrote the
reason to console.warn and nothing else — so it lived in the Railway log
buffer and expired there. From the database and the dashboard the outage was
invisible: a number going up, with no cause attached.

Every caller already computes a precise reason ("embedding failed: …",
"quality gate failed after N attempts (slop=…)", "insert failed: …"). It was
simply not being kept. Now it lands on the row, truncated, with a fallback
write so a keyword that cannot record its reason is still marked failed rather
than retried forever.

The quota path gets the same treatment even though it requeues rather than
fails, because it is the one outcome that leaves no trace anywhere: the
keyword looks untouched, no article row is created, nothing reaches the model,
and the dashboard shows a healthy queue while the site quietly stops
publishing. That is the exact shape of this outage.

Diagnosis so far, recorded here because the evidence is easy to lose: article
generation is dying BEFORE the writer model. ai_usage shows zero lx_article
calls since 2026-08-19 while buyer_journey_keyword_research ran today on the
same OpenAI key; credits are not the blocker (3,583 on the account, 20 per
article); and text-embedding-3-small answers normally when called directly.
The next 09:00 run will now say which step it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013cbcXdpLFUgJZuVfCB4Gzv
The 09:00 failures were an OpenAI 429 — "You have no credits remaining" —
found in the Railway logs of the deployment that was live at the time. 308
keywords were marked failed for a reason that had nothing to do with their
content, and the researched spend behind them was about to be thrown away.

REQUEUE=1 emits the update that returns the survivors to the queue instead of
the delete that removes the bad rows. Survivors only: of the 308, just 44 pass
today's gate, because most predate it. Requeuing all of them would have
republished the spam this whole change set exists to stop, so the same gate
that decides what may be created decides what may come back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013cbcXdpLFUgJZuVfCB4Gzv
…ality

# Conflicts:
#	scripts/purge-constructed-keywords.ts
@ralyodio
ralyodio merged commit 4f8dd1a into master Aug 28, 2026
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

40 finding(s)

HIGH/CRITICAL: 3 | MEDIUM: 28 | LOW: 9

Severity Rule Location
HIGH tls-verification-disabled lib/onion.ts:47
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
HIGH sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM redos-nested-quantifier lib/careers/jobs.ts:139
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:130
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:98
MEDIUM redos-nested-quantifier lib/sp/parseHandle.ts:92
MEDIUM redos-nested-quantifier lib/tracker/agent-gate.ts:61
MEDIUM sql-template-interpolation scripts/detect-slot-themes.ts:31
MEDIUM sql-template-interpolation scripts/purge-constructed-keywords.ts:163
MEDIUM sql-template-interpolation scripts/purge-offniche-keywords.ts:124
LOW secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
LOW secret-generic-credential lib/sp/platforms/linkedin.ts:25
LOW js-dynamic-code-execution tests/careers-page-templates.test.ts:21
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:19
LOW js-dynamic-code-execution tests/careers-widget-script.test.ts:69
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
LOW js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio deleted the autoblog-keyword-quality branch August 28, 2026 16:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant