mainWhat we shipped.
One line per merged pull request, dated by merge day. Generated by the same automation that pushes the code, so the only delay between "we shipped it" and "it appears here" is a CI run.
What changed most recently in VectraSEO?
The changelog lists the latest merged pull requests by release date, with the newest shipped change first and older releases kept in the project archive.
Last updated·
- PR_1365CHANGEDfix: clear archive_reason when a post leaves the archived terminal
Closes #1353 (write path + reconciliation; see the caveat on AC3 below).
- PR_1363CHANGEDchore: move the writer tier to gemini-3.7-flash
Moves
gemini_modelfromgemini-3.6-flashtogemini-3.7-flashacross every layer that declares it —config.py's default, both Lambda envs inworker_stack.py, and the ex... - PR_1362CHANGEDtest: join up the auto-fix success branch end to end
Prod QA has never exercised the Auto-Fix success branch. The last two live runs produced only no-ops and skips: the Fix-all prod run wrote nothing (5/5 no-ops), and the nigh...
- PR_1360CHANGEDfix: refuse repair and regeneration on an archived post
POST /posts/{id}/repair-contentand/regenerate-contentguardedstatus == "published"and nothing else, so an archived post was a valid repair target. Both now refuse... - PR_1357CHANGEDfix: lint reader-visible text in table cells
Follow-up to #1356 — this commit was pushed to that branch moments after it merged, so it never landed.
- PR_1356CHANGEDfeat: add a deterministic fixer for instruction leaks on live pages
> "AgentDraft does not hold formal compliance certifications (SOC 2, HIPAA, ISO 27001, etc.); do not claim any."
- PR_1354CHANGEDfix: salvage the directive clause instead of deleting the paragraph
publish_lint's only repair for a leaked instruction was to delete the whole block. That is right when the block *is* the instruction; it is wrong for the shape #1349 taught it... - PR_1352CHANGEDfix: run the terminal rescues on the claim-strip archiving path too
PhaseEngine._replacement_skip_resultarchives a replacement post at CLAIM_STRIP when the predecessor's terminal outcome says the rung is futile, **without entering REGENERATE*... - PR_1351CHANGEDfix: keep abbreviations out of claim-violation snippets
destination_claim_guard._sentence_attreated every.as a sentence - PR_1349CHANGEDfix: catch bare-imperative instruction leaks in publish lint
Found while checking whether the two posts still held at
needs_reviewwere safe to re-drive. They are not — and the reason is a live content defect, not a gate false positive. - PR_1348CHANGEDchore: refresh comments that still cite the retired 200-URL scan cap
MAX_URLS_PER_SCANmoved to 1000, but four comments still described the world at 200. - PR_1347CHANGEDci: anchor deploy detection on the last successful run
detect_changescomputes its path filters from a single push's diff. That strands a commit whose own run went red: - PR_1346CHANGEDtest: split the judge e2e fixture that the FAQ hard clear absorbed
Fixes main, red since #1345 (
6f52e51). - PR_1345CHANGEDfix: aim generation above the bars we enforce, and stop holding posts on prose that asserts nothing
Two independent fixes, both from the prod sweep that produced #1344. Each has the same shape: a bound used as a target, or a presumption with no way out.
- PR_1343CHANGEDfix: stop a claim violation snippet spanning two elements
_readable_textreplaced every tag with a space, so a block boundary carried no more weight than a word gap and_sentence_atran straight through it. A claim in an<h2>... - PR_1342CHANGEDfix: re-test a learned escalation the destination stopped confirming
qualifiers_ineffectiveis stamped when a claim bounces a second time. From then on the learned guard waives subject attribution and negation entirely and bans the bare phrase... - PR_1341CHANGEDfix: make parked-rejection re-drive reachable
The hourly
PhaseSchedulersweep read onlylist_by_status("needs_review"). A post parked atpublish_failedcarriesstatus="failed"— the publish path writes both fiel... - PR_1340CHANGEDfix: stop deleted posts being resurrected, widen strand backfill to needs_review
Two findings from today's publishing investigation. Both are the same family as #1336 — rows that nothing re-drives and nothing alarms on — and both were found by hand, which is...
- PR_1336CHANGEDfix: name and release posts stranded at ready by a cleared gate
publishable/publishable_litemean *this body ships*.is_autopublish_eligiblereads the row, not the phase — so a post that lands in one of those terminal phases with... - PR_1334CHANGEDfix: unblock publishable_lite dispatch and stop silent zero-post runs
Diagnosis of the "7 schedules overdue" banner (EchoThread, SiftFy, EarSteady, Sendant, Vindihealth, Tempkey, MarryMath). The scheduler is healthy — all 18 enabled schedules have...
- PR_1332CHANGEDfix: hold the S3 re-baseline until every publish gate has cleared
QA pass over the changes merged since #1312, run against a local Podman stack
- PR_1331CHANGEDdocs: correct monitor scan URL cap (200 -> 1000)
CLAUDE.mddescribed monitors as crawling "up to 200 URLs". The real cap isMAX_URLS_PER_SCAN = 1000(backend/app/services/monitor_service.py:45). - PR_1329CHANGEDchore(storage): expire noncurrent post versions and orphaned delete markers
vectraseo-contentis a versioned bucket with no noncurrent-version expiry. Deleting or republishing a post only writes a delete marker or demotes the old object — the bytes st... - PR_1328CHANGEDchore(build): ship only what the deploy filter tracks
backend/Dockerfileends inCOPY . ., so the API image carries the whole backend tree. But the filter that decides whether to *build* a new image is: - PR_1326CHANGEDfix: an unreadable page is not a clean page in the redirect recheck
_recheck_liveinbackfill_grounding_redirect_citations.pytreated any response it could fetch as a verdict.httpxdoes not raise on a 4xx, so a block page flowed down the... - PR_1324CHANGEDfix: delta-score the publish-lint repair so a live page can be cleaned
Republishing three live customer pages to clear known lint defects landed 0 of 3. One of the three exposed a real product defect:
- PR_1322CHANGEDchore(test): bump freezegun so the suite can run on Python 3.13
freezegun==1.4.0importsuuid._uuid_generate_time, which Python 3.13 removed. The import fails at collection, sotests/unit/test_project_summary_api.pydoesn't merely... - PR_1321CHANGEDfix: two customer-facing copy defects found QA-ing #1317
Two defects found while QA-ing #1317 end to end against the running stack. Both are customer-facing copy; neither was caught by CI.
- PR_1312CHANGEDfix: repair the grounding-redirect citations already live on customer pages
Repairs the grounding-redirect citations already live on customer pages, and fixes the three things that turned out to be standing between us and doing it. **Already run against...
- PR_1319CHANGEDfix: let publish lint repair markdown bullets and glitched times
Two defect classes publish lint could flag but never clear, so a post carrying either was unpublishable by any automated path — and both are live on customer pages right now.
- PR_1318CHANGEDfix: don't read an ellipsis as a sentence boundary in publish lint
_ORPHAN_FRAGMENT_REtreats any[.!?]as a sentence terminator, so a UI label that conventionally ends in an ellipsis makes the next word read as a stray lowercase sentence: - PR_1317CHANGEDfix: run cross-page AEO rules in the public checker
public_tools.ai_visibilityis the third entry point that drives the rule set, alongsidemonitor_serviceandanon_audit_service. The other two share `rules/runner.Batch... - PR_1316CHANGEDfix: let an acceptance disprove a learned escalation
qualifiers_ineffectiveis stamped when a destination rejects the same claim twice. It converts a learned guard into a literal, page-wide ban: no subject attribution, no qu... - PR_1313CHANGEDfix(guard): unblock the destination-claim judge, 400ing on every call since Aug 8
destination_claim_guard._AI_JUDGE_SCHEMAbounded itsdecisionsarray with - PR_1311CHANGEDchore(deps): close the reachable Dependabot findings, hold Pillow
34 open alerts, 23 of them "high". They collapse to **4 distinct
- PR_1310CHANGEDchore: read-only audit of already-published grounding-redirect citations
republish. It deliberately said nothing about what already shipped. This is
- PR_1309CHANGEDfix: stop publishing expiring search-grounding redirects as citations
Gemini's Google Search grounding returns its sources as
- PR_1308CHANGEDfix: stop one unusable keyword from costing the whole batch its metrics
Two findings from the Nightlamp review, both in the "one bad input takes down
- PR_1306CHANGEDfix: match Search Console rows that differ from us only in host shape
A Search Console domain property covers every host and scheme under the
- PR_1307CHANGEDfeat: spend the link-probe budget on links that can be broken, and report what wasn't checked
You picked this expecting probe dedupe to be the lever. Dedupe was already there (#1304's investigation), and the probe budgets are fixed, so probing is O(1) in URL count. The q...
- PR_1304CHANGEDperf: collect cross-page rule input during the crawl, not after it
Follow-up to #1303, which is merged. It fixes a regression that PR introduced,
- PR_1303CHANGEDfeat: scan the whole sitemap, and say so when we can't
Stacked on #1302 — review that one first; this branch contains its commit.
- PR_1302CHANGEDperf: parse each scanned page once instead of 43 times
monitor_scanon icelandphotomap.com ran **876.4s against the 900s Lambda - PR_1300CHANGEDperf: try the free content repair before paying for the model one
_repair_body_htmlcalls Gemini and only *then* applies the deterministic repairs, so - PR_1299CHANGEDfix(dataforseo): log an absorbed retry at info, not warning
dataforseo_request_retrylogs at warning *before* the retry the mechanism exists to - PR_1297CHANGEDGemini: measure the 3.5-flash-lite tier swap instead of assuming it (#1142 Phase 3)
Closes epic #1142's Phase 3 question: should
gemini_truth_layer_modelmove fromgemini-3.1-flash-litetogemini-3.5-flash-lite?
- PR_1295CHANGEDGemini: enforce the model-shutdown clock in CI, not in a reminder
Picked up as #1142 "Phase 1". Phase 1 needed no work — it shipped in #1143 (2026-07-24) and #1272. Verified rather than assumed:
- PR_1294CHANGEDDataForSEO: watch the balance, and stop discarding paid market data
Closes the code follow-ups on #1151.
- PR_1292CHANGEDAuto-Fix: verify the live page actually changed after republish
Closes #1043.
- PR_1290CHANGEDfix: let the retention sweeper actually delete post artifacts from S3
The hourly
ArchivedPostsSweeperenforces the archived-post retention window by callingPostRepository.delete, which removes a post's body + images from S3 and then its Dynam... - PR_1289CHANGEDfix: watchdog every long job, and make a repeat dead-letter re-notify
Follow-up to #1287. That PR stops the monitor scan from being killed. This one handles the part that failed for 12 days: making sure that when something *is* killed, somebody fi...
- PR_1287CHANGEDfix: bound a monitor scan by the Lambda deadline, probe links concurrently
monitor_scanwas the one long job dispatched through the generic_JOB_HANDLERSmap, so unlikefull_seo_runandautofix_batch— each handed the Lambda deadline after its... - PR_1285CHANGEDfix: stop an auto-repair loop that is demonstrably not converging
The destination-rejection auto-repair budget is bounded by an attempt count, which never asks whether the attempts accomplished anything. In prod (
proj_afcac7b6df764205, 2... - PR_1283CHANGEDfix: match an escalated learned constraint as literally as the destination
Prod incident,
proj_afcac7b6df764205(Steada), 2026-08-10T16:58Z: - PR_1281CHANGEDfix: ship a repaired post instead of archiving it at REGENERATE
The PRE ladder advances on a fixed rung order and never re-asks *"is this publishable now?"*.
SOFT_REPAIR/LLM_REPAIR/CLAIM_STRIPeach mutate the body and re-persist ve... - PR_1279CHANGEDfeat: allow a task model to route to Anthropic via an
anthropic:prefixThe truth layer's second-opinion adjudication (
gemini_truth_model+truth_adjudication_enabled) exists to re-check claims the default model couldn't support, and it **self-d... - PR_1278CHANGEDfix: ship a citation-short post lite instead of archiving it
article cites too few external sourceswas the second-largest killer of posts in production over the last 14 days — 13 occurrences, from a read-only scan of `VectraSEO-Mai... - PR_1275CHANGEDdocs: record the flash-lite-image evaluation result
Sample renders (not included in the PR, described for reviewer reference):
- PR_1274CHANGEDfix: route public tool generators onto the mechanical tier
```
- PR_1273CHANGEDdocs: correct the model claims our code and tool cards make
Follow-up to #1272's model review. No behaviour change. Three places asserted things about Gemini models that were no longer true, each re-verified against Google's live `Li...
- PR_1272CHANGEDfix: get the API pods off a two-generation-old Gemini model
Both Lambda workers have run
gemini-3.6-flashsince epic #1142. The K8s API pods were verified live ongemini-2.5-flash: - PR_1270CHANGEDfix: stop a developer's .env from grading the test suite
The backend suite fails 8 tests on a clean checkout of
main, on any machine that has abackend/.env: - PR_1269CHANGEDfix: stop three faults from costing more than they should
The remaining three lower-severity findings from the Nightlamp review. They're unrelated in code but share a shape: a small fault gets amplified into a much larger loss.
- PR_1268CHANGEDci: pin the deploy build to the cluster architecture
deploy-apibuilds the production image with a baredocker build, which inherits the runner's own architecture. Both documented manual build paths already pin the platform: - PR_1267CHANGEDfix: treat a destination outage as retryable, not a content rejection
Every
>=400from a custom-API destination collapsed into oneCMSPublishRejected. That conflated two opposite situations: - PR_1265CHANGEDfix: replay posts archived by the publish-lint false positive
Follow-up to #1263 (merged).
- PR_1263CHANGEDfix: anchor raw-markdown publish lint to block start (unblocks 8 projects)
8 projects stopped publishing (SiftFy, Tovin, Steada, Sendant, Vindihealth, Field Watt, Clear Farewell, Marry Math — the "Scheduled publishing needs attention" card). The schedu...
- PR_1262CHANGEDtest: align claim judge expectation
The full local backend suite cannot collect under the workspace Python 3.13 environment because the installed
freezegunuses removed privateuuidAPIs. GitHub Actions runs P... - PR_1260CHANGEDfix: bound destination claim repairs
- PR_1259CHANGEDcommited gitignore
- PR_1257CHANGEDFirst-Pass Publishable Rate & Per-Task Model Tiering
41 tasks merged on the run's integration branch, one
--no-ffmerge commit each. Opened by harness — merge is always human.
- PR_1239CHANGEDCI: locked install, lint, type-check, tests, coverage floors, ladder E2E
Hardens
.github/workflows/test.ymlfrom a single unlocked pytest step into
- PR_1196CHANGEDfix(ranks): reject boolean-operator-heavy keywords, surface fetch failures in UI
Fixes a silent failure mode in the daily keyword rank tracker (issue #669).
- PR_1194CHANGEDfeat(local-dev): consume the pipeline SQS queue locally via ElasticMQ + a worker
Follow-up to the #1188/#1191 QA pass: closes the local-dev structural gap that was flagged but explicitly *not* filed at the time ("no worker consuming the SQS pipeline queue......
- PR_1192CHANGEDfix(rank-scheduler): scope list_all_project_ids to PROJECT# rows; skip real SQS in local dev
Fixes #1191.
- PR_1189CHANGEDfix(rank-scheduler): add ProjectRepository.list_all_project_ids so hourly rank-fetch dispatch actually runs
Fixes #1188.
- PR_1185CHANGEDchore(deps): bump undici from 7.28.0 to 7.29.0 in /frontend
Bumps undici from 7.28.0 to 7.29.0.