- Sources read: 2 (1 X commit (Gen4+2), 1 of 30 arXiv triage items passed (SDF))
- Canon files updated: 1 (x.md, 7 sections updated)
- Radar entries: 1 (Google Discover SDF newly registered)
- Meta engineering blog posts: 0 (last relevant post 2026-08-05, already folded into the canon)
- Netflix tech blog posts: 0 (last post 2026-08-07, unrelated to algorithms)
Other changes reviewed today:
- In the same commit, X removed the condition that let only candidates with one or more favorites into the retrieval index.
- Google authors published a paper on the staleness filter (SDF) that ran in the Discover feed for two years. They reported that user staleness reports fell by 54.9%.
1Added a V2 path to ad safety verdicts and a Grok 4.5 judge to adult verdicts — X
Source xai-org/x-algorithm @ b089ce6 — "Open-source X Recommendation Algorithm" — 2026-08-17 20:20 UTC, public source code. These notes number the X commits after the Gen4 release in order, and this commit is Gen4+2. It changes 34 files, +751/−162. It is smaller than Gen4+1 (2026-08-14), but it covers three separate axes in one commit: ad safety, the adult content judge, and retrieval indexing.
Background The Gen4 release (2026-08-13, 47c1bcd) of the open-source X recommendation algorithm was very large, opening parameters, the safety pipeline, and 15 labeling sub-repos. However, some botmaker rules were not released, to prevent gaming. Gen4+1 (2026-08-14, c65aa17), which came the next day, widened the release in five directions, and the details are in daily/2026-08-16.md: weight interpretation rules, the Brazil election filter, Thompson sampling, three bool params of Phoenix (the ranking and retrieval model of X For You), and a partial release of the BDSM enforcement note generator. This commit came three days later (2026-08-17); it is much smaller, but it clearly focuses on the ads and adult content verdict layers.
Three directions in this commit affect the canon in particular: the V2 path in ad brand safety verdicts (card A), the judge layer added to adult content verdicts (card B), and the removal of the Phoenix retrieval indexing gate (card C).
What we learnedA. A V2 path appeared in ad brand safety verdicts while V1 stayed as is
V2 logic was added alongside the ad brand safety verdict (the verdict that decides whether an ad may be placed next to a post). Through early Gen4 the canon stated that compute_verdict checks MEDIUM_RISK_LABELS (14) and LOW_RISK_LABELS (3) in order to decide whether to attach an ad. However, MEDIUM_RISK_LABELS in the code had been 15 since Gen4, and the canon had left one out verified — brand_safety.rs@47c1bcd. This function lives in home-mixer/models/brand_safety.rs of home-mixer (the service that assembles the For You feed). This commit added compute_verdict_v2 next to it without deleting it, moved six Grok labels to _V2 versions, and widened the label sets to _V2 versions as well. It also introduced the strip_v1_grok_written helper, which forcibly filters out specific V1 labels.
A new function and three label sets used in V2 verdicts were introduced verified —
home-mixer/models/brand_safety.rs.Compared with V1, medium risk labels grew from 15 to 16, and low risk labels stayed at 3.
GROK_NSFAwas renamedGROK_NSFA_V2, andGROK_NSFA_EXPANDED_V2is new (one more on the medium side).GROK_NSFA_LIMITEDwas renamedGROK_NSFA_LIMITED_V2(one was moved over from the low side).Label set definitions:
pub(crate) const MEDIUM_RISK_LABELS_V2: &[SafetyLabelType] = &[ SafetyLabelType::NSFW_HIGH_PRECISION, SafetyLabelType::NSFW_HIGH_RECALL, SafetyLabelType::NSFA_HIGH_PRECISION, SafetyLabelType::NSFA_KEYWORDS_HIGH_PRECISION, SafetyLabelType::GORE_AND_VIOLENCE_HIGH_PRECISION, SafetyLabelType::NSFW_REPORTED_HEURISTICS, SafetyLabelType::GORE_AND_VIOLENCE_REPORTED_HEURISTICS, SafetyLabelType::NSFW_CARD_IMAGE, SafetyLabelType::DO_NOT_AMPLIFY, SafetyLabelType::MALICIOUS_URL, SafetyLabelType::NSFA_COMMUNITY_NOTE, SafetyLabelType::PDNA, SafetyLabelType::EGREGIOUS_NSFW, SafetyLabelType::GROK_NSFA_V2, // new SafetyLabelType::GROK_NSFA_EXPANDED_V2, // new SafetyLabelType::NSFW_TEXT, ]; pub(crate) const LOW_RISK_LABELS_V2: &[SafetyLabelType] = &[ SafetyLabelType::NSFA_LIMITED_INVENTORY, SafetyLabelType::GROK_NSFA_LIMITED_V2, // new SafetyLabelType::NSFA_HIGH_RECALL, ];
V2 verdicts first remove from the input the labels that the V1 prompt wrote twice (dual write), and decide using only the remaining labels verified (same file).
There are two removal conditions.
- The label is
NSFA_HIGH_PRECISIONorNSFA_LIMITED_INVENTORY. - The botmaker (X's rule engine) rule id of that label is one of the seven in
PROMPT_OWNED_RULES = [1400, 1410, 1420, 1500, 1510, 1610, 1700].
- The label is
A label that meets both conditions is treated as dual-written by the V1 prompt and is removed from the input label set.
This step is a migration device that makes the V2 logic ignore the dual writes made by the V1 prompt and decide from V2 labels alone.
The core logic for this is
strip_v1_grok_written:fn strip_v1_grok_written(labels: &HashMap<SafetyLabelType, SafetyLabel>) -> Cow<'_, HashMap<SafetyLabelType, SafetyLabel>> { const V1_DUAL_WRITTEN: &[SafetyLabelType] = &[ SafetyLabelType::NSFA_HIGH_PRECISION, SafetyLabelType::NSFA_LIMITED_INVENTORY, ]; const PROMPT_OWNED_RULES: &[i64] = &[1400, 1410, 1420, 1500, 1510, 1610, 1700]; let is_v1_grox_written = |label_type: &SafetyLabelType, label: &SafetyLabel| { V1_DUAL_WRITTEN.contains(label_type) && botmaker_rule_id_from(label) .is_some_and(|rule_id| PROMPT_OWNED_RULES.contains(&rule_id)) }; if !labels.iter().any(|(t, l)| is_v1_grox_written(t, l)) { return Cow::Borrowed(labels); } Cow::Owned( labels.iter() .filter(|&(t, l)| !is_v1_grox_written(t, l)) .map(|(t, l)| (*t, l.clone())) .collect(), ) }
The V2 verdict function uses the same decision order as V1. Two things differ: the label names it references and a preprocessing step placed at the front verified (same file).
The order is medium cascade → whether Grok scored → PTOS (policy-based violation category verdict) review → low → otherwise safe.
The referenced label names changed from
GROK_SFAtoGROK_SFA_V2and fromGROK_NSFA_LIMITEDtoGROK_NSFA_LIMITED_V2.Preprocessing that removes V1's dual writes was added at the very front.
Code showing the decision order of
compute_verdict_v2:pub(crate) fn compute_verdict_v2(labels: ..., tweet_id: u64) -> BrandSafetyVerdict { let labels = strip_v1_grok_written(labels); if MEDIUM_RISK_LABELS_V2.iter().any(|l| labels.contains_key(l)) { return BrandSafetyVerdict::MediumRisk; } let scored_by_grok = labels.contains_key(&SafetyLabelType::GROK_SFA_V2) || labels.contains_key(&SafetyLabelType::GROK_NSFA_LIMITED_V2); if !scored_by_grok { return BrandSafetyVerdict::MediumRisk; } if tweet_id >= PTOS_CUTOFF_TWEET_ID && !labels.contains_key(&SafetyLabelType::PTOS_REVIEWED) { return BrandSafetyVerdict::MediumRisk; } if LOW_RISK_LABELS_V2.iter().any(|l| labels.contains_key(l)) { return BrandSafetyVerdict::LowRisk; } BrandSafetyVerdict::Safe }
X states in a test that V1 and V2 must produce the same verdict verified (same file).
In the same file,
v2_mirrors_v1_across_tier_matrixasserts that the two verdicts match across 6 label combinations × two tweet_id points (before and after PTOS_CUTOFF).The two verdicts must not diverge during the migration so that ad adjacency does not change, so this test acts as a safety net.
Test code:
#[test] fn v2_mirrors_v1_across_tier_matrix() { fn to_v2(v1_set: &[SafetyLabelType]) -> Vec<SafetyLabelType> { /* V1 → V2 label name conversion */ } let matrix: &[&[SafetyLabelType]] = &[ &[], &[SafetyLabelType::GROK_SFA], &[SafetyLabelType::GROK_SFA, SafetyLabelType::PTOS_REVIEWED], // ... 6 scenarios ]; for v1_set in matrix { let v2_set = to_v2(v1_set); for tweet_id in [PRE_CUTOFF_ID, POST_CUTOFF_ID] { assert_eq!( compute_verdict(&labels_with(v1_set), tweet_id), compute_verdict_v2(&labels_with(&v2_set), tweet_id), ); } } }
This change is a concrete example of how a label schema migration is handled in real production, and it connects to the caveat list's way of identifying "dead files".
- The ad adjacency verdict differs depending on whether V1 and V2 are both alive and whether
worst_verdictcombines the two results.
- The ad adjacency verdict differs depending on whether V1 and V2 are both alive and whether
Grok's safety labels themselves are versioned, and the migration is implemented as logic in the code that detects dual-write.
- The canon has recorded ad brand safety from Gen3 through Gen4 as a "hand-written rule cascade".
- This also connects to the "dead files" test. Because both V1 and V2 are exported, which one is actually called must be checked in the upstream hydrator (
ads_brand_safety_vf_hydrator.rs). verified This commit also modified that hydrator (+13/−7), which can be read as a sign that the upstream call path is switching from V1 to V2.
Which rules the seven rule ids marked as owned by the botmaker prompt actually are is not public.
PROMPT_OWNED_RULES = [1400, 1410, 1420, 1500, 1510, 1610, 1700]is marked by its name as owned by the botmaker prompt.- However, exactly which rule in
botmaker-rules/, released in Gen4, each id corresponds to is not public, because there is no mapping document linking rule ids to rule files. - We add this mapping to the canon's deep dive list.
What we learnedB. When adult content verdicts disagree, a Grok 4.5 judge decides again and can lower hard to soft
A judge layer was added to adult content verdicts for the case where verdicts disagree. The safety pipeline of grox (the Grok-based nearline annotation pipeline), first released in Gen4, ran two verdicts in parallel and only logged when the results differed. One is TaskSafetyPtosSafemodelSexNudity (a sex/nudity verdict using its own vision model), and the other is PtOS (policy-based violation category verdict).
This commit split that logic into two files. safemodel now only makes the verdict (_compare_outcome was removed). The new file task_safety_ptos_adult_content_cross_validation.py calls the Grok 4.5 judge to decide again when the safemodel and PtOS verdicts disagree; the disagreements are the case where safemodel judged positive but the PtOS policy did not catch a hard violation, and the reverse. If the judge answers "not hard", the final decision is demoted to AdultContentSexualSoft, and the positive mark left by safemodel is also cleared. The verdict layers grew to three: safemodel, PtOS, and the Grok 4.5 judge.
- Cross validation is handled in four steps by the new file
task_safety_ptos_adult_content_cross_validation.py(162 lines) verified —grox/flows/ptos/task_safety_ptos_adult_content_cross_validation.py.
- First, it compares the two verdicts to find a disagreement verified (same file).
- A new enum
CompareOutcomedistinguishes four cases:BOTH_POSITIVE,BOTH_NEGATIVE,SAFEMODEL_ONLY_POSITIVE, andPTOS_ONLY_POSITIVE. is_disagreementis true when the outcome is SAFEMODEL_ONLY or PTOS_ONLY.- This logic was inside
safemodel_sex_nudity.pyin Gen4; this commit split it out and moved it to a new file.
- A new enum
- It runs only in the deluxe flow and skips posts already flagged as NSFW verified (same file).
is_deluxe = ctx.payload.task_type == SAFETY_PTOS_DELUXE, so it runs only in the deluxe flow. The regular flow only logs, as before.- Whether a post is flagged is checked by
_post_is_already_flagged_nsfw, which looks upStratoSafetyPostAnnotationsResultDirectMh.
- If there is a disagreement, it calls the Grok 4.5 judge verified (same file).
- The new class
SafetyPtosAdultContentCrossValidationJudgecalls theModelName.EAPI_GROK_4_5_X_ALGOmodel. - The system message contains
adult_content_policy_prompt(). - The user message contains
PostRenderer.render(post, include_reply_to=True)andUserRenderer.render(post.user). - It parses the response in
<json>...</json>format to obtain aSafetyPolicyobject. - This call has a separate circuit breaker,
_EAPI_4_5_X_ALGO_BREAKER_CONFIG. It is set to a 50% failure rate, a 600-second window, 600-second recovery, and 5 half-open attempts.
- The new class
- If the judge answers "not hard", it lowers the verdict to soft verified (same file).
- If the judge answers
AdultContentSexualHard, the original verdict is kept as is. - Otherwise, it does the following three things.
- It demotes
safetyPolicy.policyTypeof every AdultContent violation toAdultContentSexualSoft. - It marks the demoted violations with
reason = "Grok 4.5 Cross Validation disagreed". - It clears
safemodel_sex_nudity.positiveto false.
- It demotes
- Whatever the original reason for the verdict was, the final audit log records the fact that "it was demoted because the Grok 4.5 judge disagreed" as explicit text.
- If the judge answers
task_safety_ptos_safemodel_sex_nudity.pywas also cleaned up in the same commit verified —grox/flows/ptos/task_safety_ptos_safemodel_sex_nudity.py.- It removed
_compare_outcome, the comparison log, and the CompareOutcome enum definition, which were moved to the cross_validation file. - It added a pre-gate that skips the verdict itself when, in the deluxe flow, the post is already flagged as NSFW.
- It explicitly sets
safemodel_sex_nudity.scored = Truewhen the verdict finishes. cross_validation looks at this flag to decide whether to run. - In other words, the two files depend on each other. When safemodel runs first and leaves
scored=True, cross_validation sees that flag and runs. It then compares the verdict results and, if needed, asks Grok 4.5 for a new verdict.
- It removed
The Grok 4.5 judge call strips from the system prompt the constraint that hides the reasoning process verified —
grox/flows/ptos/classifier.py:597-608._strip_thinking_restrictions()wraps the system prompt.The original prompt has a constraint equivalent to "do not expose the reasoning process in such answers". In the judge context it strips that constraint so the reasoning can be seen.
Code that builds the prompt:
convo.messages.append(Message( role=Role.SYSTEM, content=[_strip_thinking_restrictions(adult_content_policy_prompt())], )) user_msg = Message(role=Role.USER, content=[]) user_msg.content.extend(UserRenderer.render(post.user)) user_msg.content.extend(PostRenderer.render(post, include_reply_to=True)) user_msg.content.append( f"\n\nAnalyze the post {post.id} for the specific safety policy violation category: {SafetyPolicyCategory.AdultContent.value}" ) user_msg.content.append( f"\n\nProvide the requested JSON object for the specific safety policy type.{THINKING_CONTROL_START}" )
For a reply, the parent post is also put into the context verified (same file).
- This is the result of setting
include_reply_to=True. - It means that which post the reply responds to affects the adult content verdict.
- This is the result of setting
The judge leaves its reasoning as free text and returns only the final verdict as JSON verified (same file).
- The response is parsed with
re.compile(r"(.*)<json>(.*)</json>", re.DOTALL). - The response format splits into free text and JSON inside
<json>...</json>tags.
- The response is parsed with
This change shows a move toward rebuilding the verdict itself as an ensemble of several models.
- Until now the canon has recorded the verdict structure as "single verdict (safemodel or PtOS or Grok) → visibility rules → filter/ad adjacency".
- The three layers use different models. safemodel is its own vision model, PtOS is a policy verdict model, and Grok 4.5 is an LLM judge.
- A distinguishing feature is that the final verdict is adjusted automatically by using the situation in which verdicts disagree. This points in the same direction as Gen4's "full release of the labeling pipeline".
It is also an important observation that verdicts only move in the demoting direction (hard → soft) and never in the escalating direction.
- This commit contains no logic in which the judge catches a hard case the original verdict missed and escalates it.
- In other words, this judge layer is designed to work only in the direction of easing excessive gating.
- This design lets us confirm in code that the policy direction of adult content gating is toward reducing false positives, not false negatives.
There are two things to watch going forward.
- We need to check whether this judge layer expands to other categories (violence, hate, spam).
- We need to check how much of the cost of using Grok 4.5 (inference cost) can be absorbed.
What we learnedC. The indexing condition of one or more favorites is gone, so candidates from fav events go to the index regardless of count
The Phoenix retrieval indexing gate was effectively removed. build1FavIndex in phoenix-rankall-strato (the event layer that decides which index a post belongs to) was, as its name says, a function that "puts posts that received one or more favorites into the index". buildMMEmbMetadataDump (the function that dumps multimodal embedding metadata to Kafka) also ran only after passing the same favoriteCount >= 1 gate.
This commit removed both gates. As a result, VF-passing candidates that arrive through fav events and are not replies or reposts flow into the "1fav" index and the multimodal embedding dump regardless of fav count. However, the name "1fav" remained, and the case where a candidate arrives with 0 favs is only instrumented with a new counter, 1fav_uec_count_race.
Two functions changed in
phoenixRankAllCandidateProcessor.strato, which creates the indexing events verified —phoenix-rankall-strato/columns/phoenix_rank_all/phoenixRankAllCandidateProcessor.strato.The range in which the two functions are called is unchanged verified — same file L421-458.
- Dispatch calls the two functions only when
eventSource == Fav, and before that it skips community posts, replies, and reposts. - Posts that VF drops (
shouldDropPostByVF) and adult posts do not reachbuild1FavIndex. Only thefavoriteCountgate inside the function is gone. - However, a Fav event on an adult post with immersive video separately calls
buildMMEmbMetadataDump(same file L438-442).
- Dispatch calls the two functions only when
build1FavIndex, which builds the 1fav index, now publishes an indexing event every time regardless of fav count, and only the fav = 0 case is instrumented with a counter verified (same file).The counter used for instrumentation is
1fav_uec_count_race.Before the change:
def build1FavIndex(candidate: PhoenixRankAllCandidate, stats: Stats.StatsReceiver): Unit = { candidate.engagementCount match { case Some(ec) => if(ec.favoriteCount.getOrElse(0) >= 1) { val object = { postId = candidate.postId, authorId = candidate.authorId, indexName = "1fav" } #<hydra/kafka/indexingEventKafkaTopic>.insert((), object) stats.counter("1fav_indexing_event_created").incr(1) } case None => () } }After the change:
def build1FavIndex(candidate: PhoenixRankAllCandidate, stats: Stats.StatsReceiver): Unit = { if (candidate.engagementCount.flatMap { ec => ec.favoriteCount }.getOrElse(0) == 0) { stats.counter("1fav_uec_count_race").incr(1) } val object = { postId = candidate.postId, authorId = candidate.authorId, indexName = "1fav" } #<hydra/kafka/indexingEventKafkaTopic>.insert((), object) stats.counter("1fav_indexing_event_created").incr(1) }
"race" in the counter name appears to refer to the possibility that fav had not yet been reflected because the timing of the uec (user engagement counts) store and the candidate arrival do not line up inferred.
The separate function
buildMMEmbMetadataDumpalso removed the samefavoriteCount >= 1gate, so it now runs for candidates in the same range regardless of fav count verified (same file).- This part looks up three versions of multimodal embeddings, v3, v5_1, and v6_dev, and dumps them to Kafka.
- Before the change it was
if(ec.favoriteCount.getOrElse(0) >= 1) { ... multimodal embedding fetch + Kafka dump ... }. - After the change there is no gate, so it looks up the three multimodal embedding versions and dumps them to Kafka regardless of fav count.
This change directly conflicts with the understanding, recorded in the canon from Gen3 through Gen4, that "the index carries popularity signals".
- It means that even posts that came in through a fav event but whose count is still 0 enter the 1fav index.
- The understanding that "phoenix-rankall updates the index with popularity signals", which the canon has observed from Gen3 through Gen4, is no longer accurate.
- From today on, the index name "1fav" is only a label kept for backward compatibility, and the actual index holds candidates that came in through fav events regardless of count.
- This observation is enough evidence to add "an index name does not imply a filter condition" to the canon's caveat list.
Removing the gate on the multimodal embedding dump deserves particular attention.
- Looking up all three embeddings v3, v5_1, and v6_dev even for unpopular posts and dumping them to Kafka is a choice that greatly raises the inventory preparation cost of providing retrieval signals.
- This runs opposite to the "must have popularity to enter as a candidate" that the canon has stated, and it appears to be work that expands the infrastructure so Phoenix retrieval can attempt embedding-based matching on long-tail posts too inferred.
Two metrics will show up in upcoming commits.
- See what proportion the actual observed values of the
1fav_uec_count_racecounter show. - See which param gate the expanded index coverage is activated together with. Examples are the top-k of Phoenix retrieval or the SID (semantic ID, a token sequence that represents an item as hierarchical codes) window.
- See what proportion the actual observed values of the
Canon update Updated 7 sections of services/x.md.
- Added a summary of the Gen4+2 follow-up release (brand safety V2, Grok 4.5 cross validation, indexing gate removal) to the "In one line" section.
- Added a Gen4+2 row to the "Open-source Gen4" generation table (commit b089ce6, +751/−162, 34 files).
- Added to the "Ads" section the brand safety V2 label sets (16 medium and 3 low), the
strip_v1_grok_writtenmigration helper, and thev2_mirrors_v1_across_tier_matrixtest. - Created the section "Grox → PtOS Adult Content Cross-Validation (new in Gen4+2)", summarizing the four-step flow, the judge prompt details, and the characteristics of the demotion direction.
- Added to the "Phoenix training and index" section the observation that the indexing gate was removed in
phoenix-rankall-strato: the new1fav_uec_count_racecounter and the removal of the multimodal embedding dump gate. - Created a subsection "Resolved by the Gen4+2 release (2026-08-19)" in the Open questions section, and added the mapping between
PROMPT_OWNED_RULESand botmaker rules to the deep dive list. - Added 4 entries to the Sources section: the b089ce6 commit, brand_safety.rs, adult_content_cross_validation.py, and phoenixRankAllCandidateProcessor.strato.
Why it matters X's release rhythm appears to have settled through the third release, at three-day intervals: Gen4 (2026-08-13) → Gen4+1 (2026-08-14) → Gen4+2 (2026-08-17). What matters in particular is that each release differs in nature. Another feature of this release is that ad brand safety and adult content verdicts were each refined in a different direction in one commit.
- Gen4 released parameters and the labeling pipeline (excluding some botmaker rules). It is a large release that opens up the structure.
- Gen4+1 contained weight interpretation, regulatory response, A/B connections, and model input expansion. It is a release widened in five directions.
- Gen4+2 contained the ad brand safety V2 migration, the Grok 4.5 judge layer, and removal of the retrieval index gate. It is a release that refines the verdict layers.
- The V2 migration corrects the label schema, and cross-validation expands the verdict layer.
- There is one more reason to see X as settling Gen4's release rhythm into "large release → one-day follow-up cleanup → layer-by-layer refinement a few days later" observed 2026-08-19.
- The canon can gain two things from this flow.
- The value of tracking X's commit log daily keeps growing, because each commit touches a different axis.
- Early signs that X's open-source practice is moving in particular directions (regulatory response, safety verdict layers, retrieval coverage) are accumulating across the three releases.
Five items need a deep dive but were not handled today.
- Watch when the label migration from V1 to V2 completes. We need to check whether a commit deleting the V1 function appears or whether dual-write is kept permanently.
- We need to check which of the actual rule files in
botmaker-rules/the seven botmaker rule ids inPROMPT_OWNED_RULES(1400/1410/1420/1500/1510/1610/1700) correspond to. - We need to check whether the Grok 4.5 judge layer expands to other categories (violence, hate, spam).
- Watch the actual observed ratio of the
1fav_uec_count_racecounter. It cannot be observed directly from outside, but it can be observed indirectly by watching follow-up commits adjust thresholds. - We need to check where the consumers of the three multimodal embedding versions v3/v5_1/v6_dev are. The canon already records that it is not home ranking, but we have not yet confirmed where the embeddings go.
2A filter that splits stale content into two kinds ran in production for two years — Field radar (Google Discover)
Source Decomposing Staleness in Recommender Systems: A Dual-Filter Framework for Supersession and Decay — arXiv:2608.15780 — submitted 2026-08-16, paper. All six authors are affiliated with Google LLC. We confirmed the author affiliations in the HTML version, and all email domains are @google.com. It was accepted to the CIKM Applied Research Track 2026, and the DOI is 10.1145/3799682.3840082.
Background Google Discover is a personalized feed surface offered on the Android home screen and in the Google app, with hundreds of millions of DAU and billions of MAU verified (paper abstract). The canon has not registered Google Discover as a tracked service so far, because Google has never published Discover's algorithm, ranking, and retrieval layers in full. This paper covers only that surface's staleness filter (a filter that removes stale candidates), so it is the release of a single component, not a full architecture disclosure. Even so, it contains an actual A/B result statement that the filter was deployed in production for two years and improved a user feedback metric by 54.9%, which meets the radar acceptance criteria (platform-company authors, production deployment, large scale).
What we learnedA. The paper splits "staleness" into two mechanisms and predicts each with an independent model
The problem this paper defines is to separate two distinct mechanisms by which a candidate in a content feed is judged "stale" and to predict each with an independent model.
- Supersession is the case where a newly arrived item invalidates the information value of an earlier item.
- For example, once a sports player's team transfer is announced, earlier articles about the transfer negotiations automatically become stale.
- The basis for the judgment is not time itself but whether a later item exists.
- Relevance decay is the case where an item's own information value declines naturally over time even when no later item exists.
- For example, a "meteor shower tonight" notice is useful that evening but worthless from the next day.
- The basis for the judgment is a curve over time.
- Existing practices (age cutoff, engagement heuristic) cannot distinguish these two mechanisms. This causes two problems.
- An age cutoff greatly overestimates or underestimates relevance decay.
- An engagement heuristic is a lagging signal, so it reacts only after stale content has already been shown.
What we learnedB. The two filters are combined with OR and remove candidates before ranking
- SDF consists of two filters verified — arXiv 2608.15780v1, Section 3 Methodology.
- The Relational Staleness Filter judges supersession and uses a pairwise classification framework verified (same section).
- For a pair of a candidate item
d_tand a topic-related itemd'that arrived after it, a learned functionf(d_t, d')makes a binary judgment of whether "d' makes d_t stale". - The disjunction of these judgments (∃ d' > t: f(d_t, d') = 1) becomes the supersession indicator.
- Manual annotation of the training data is impossible at this scale, so it is synthesized with an LLM pipeline.
- For serving, a student model obtained through knowledge distillation is deployed.
- For a pair of a candidate item
- The Predicted Traffic Ratio (PTR) Filter is a multi-horizon regression model that judges relevance decay verified (same section).
- It takes an item's multimodal content (text, image, video) as input and predicts the traffic ratio (normalized visit traffic) at several future horizons.
- The training label is the item's lifetime visit traffic, that is, the visit volume over its whole lifetime.
- If the traffic ratio at serving time is lower than a threshold, the decay indicator becomes 1.
- The two filters are combined by disjunction (OR) and placed upstream (the candidate pruning stage before ranking) verified (same section).
- If either supersession or decay is flagged, the candidate is removed.
- Fewer candidates are passed to ranking, so downstream serving cost also decreases.
What we learnedC. After two years in production, user-filed staleness reports fell 54.9%
After two years of production deployment (initial → expansion → stabilization), user-filed staleness reports decreased by 54.9% verified — arXiv 2608.15780v1, Section 4.3 Online Deployment.
- This is measured on in-product user feedback and compared with the pre-deployment baseline.
- Split by report type, supersession reports fell 64.0% and decay reports fell 34.4%.
- The paper writes that the per-type reductions are mainly due to the corresponding filter, but they are not values that measure the standalone contribution of a single filter turned on alone.
The filters were tuned under an engagement-neutral constraint verified (Section 4.2 Online A/B Testing).
- This means that overly aggressive cutting would improve the staleness metric further but reduce engagement, so such an approach was ruled out.
"Configurations are tuned to be engagement-neutral; trivially aggressive cuts could reduce staleness much further but at the cost of substantially shrinking the candidate set and degrading user engagement, which we explicitly rule out." — Same paper, Section 4.2
Under this constraint, each filter alone produces a statistically significant negative Δstale verified (same section).
- Δstale is the difference in stale ratio between items for which the treatment group increased impressions and items for which it decreased them; a negative value means the treatment group reduced staleness.
- The Δstale values in Table 3 are −4.19% for relational, −3.10% for intrinsic, and −6.91% for SDF overall.
- The engagement improvement (feed positive engagement +0.26±0.18%) is not a per-filter value; it was measured in a two-month joint-holdback experiment with both filters turned off.
Online staleness was sampled weekly from the control and treatment groups of the A/B arms and observed for four consecutive weeks (weighted by the size of the view delta) verified (same section).
Canon update Added a new SDF entry (Google Discover — two-year production staleness filter, dual supersession + decay learning) to the "new this month" section of radar.md. Its status is recorded as "watching".
Why it matters This paper may affect the canon in three directions.
- It becomes an axis of comparison with X's stale-post handling.
- The
IS_STALE_POST14Dbool feature of X observed in Gen4+1 (2026-08-14) (daily/2026-08-16.md) is rule-based handling: "after 14 days, stamp the engagement count to 0 and set the bool". - SDF, by contrast, is a fully learning-based approach that predicts supersession with a learned model and decay with a lifetime-traffic regression.
- From the canon's perspective, the two approaches reflect two different definitions of staleness.
- X gives the ranker an explicit label to mitigate the self-reinforcing loop in which reduced impressions lead to count zero. Google Discover removes items from the candidate pool entirely, that is, it prunes before retrieval.
- The positions where the two handlings sit are fundamentally different.
- The
- It is a promotion candidate for making staleness on feed surfaces a topic.
- The current canon topic list has no staleness entry.
- We have not yet confirmed how Netflix Foundation Model or Meta ConnectionMind handle stale items.
- If a second company produces a similar statement, we will consider creating a new section in
topics/.
- Google Discover is not registered as a tracked service for now.
- This paper released only a single component, the staleness filter, and the rest of the algorithm stack, including retrieval, ranking, and diversity, is still not public.
- Creating a canon file in that state does not fit this repository's principle that "a service canon is a compressed conclusion".
- We keep it on the radar and will revisit it when a second component is published.
What to check next
- Check X's next commit (Gen4+3) — if the release rhythm of one-day and three-day gaps (8-13 → 8-14 → 8-17) holds, the likely candidates for the next commit are the following four.
- A diff updating the account list of the Brazil filter
- The next step of the label migration from V1 to V2 (removing the V1 function or adding another label set)
- A change expanding the Grok 4.5 judge layer to other categories
- A param gate paired with expanding the index coverage of
phoenix-rankall
- Check SDF follow-up statements — confirm whether Google Discover publishes other components (retrieval, ranking, diversity) in the CIKM/WWW 2027 cycle. Whether to promote Discover to a tracked service will be decided after a second component is published.
- Keep watching the Kuaishou, Alibaba, and ByteDance lines — these three companies have been steadily publishing production papers this month. We will keep observing sources that pass arXiv triage for the rest of August.