0x00 한

Research / Daily reports

  • 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)
In one lineX added a V2 path alongside the ad brand safety verdict and made conflicting adult content verdicts go back to Grok 4.5. If the judge disagrees, a hard verdict is lowered to soft.

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_NSFA was renamed GROK_NSFA_V2, and GROK_NSFA_EXPANDED_V2 is new (one more on the medium side).

    • GROK_NSFA_LIMITED was renamed GROK_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_PRECISION or NSFA_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].
    • 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_SFA to GROK_SFA_V2 and from GROK_NSFA_LIMITED to GROK_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_matrix asserts 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_verdict combines the two results.
  • 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.

  1. First, it compares the two verdicts to find a disagreement verified (same file).
    • A new enum CompareOutcome distinguishes four cases: BOTH_POSITIVE, BOTH_NEGATIVE, SAFEMODEL_ONLY_POSITIVE, and PTOS_ONLY_POSITIVE.
    • is_disagreement is true when the outcome is SAFEMODEL_ONLY or PTOS_ONLY.
    • This logic was inside safemodel_sex_nudity.py in Gen4; this commit split it out and moved it to a new file.
  2. 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 up StratoSafetyPostAnnotationsResultDirectMh.
  3. If there is a disagreement, it calls the Grok 4.5 judge verified (same file).
    • The new class SafetyPtosAdultContentCrossValidationJudge calls the ModelName.EAPI_GROK_4_5_X_ALGO model.
    • The system message contains adult_content_policy_prompt().
    • The user message contains PostRenderer.render(post, include_reply_to=True) and UserRenderer.render(post.user).
    • It parses the response in <json>...</json> format to obtain a SafetyPolicy object.
    • 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.
  4. 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.policyType of every AdultContent violation to AdultContentSexualSoft.
      • It marks the demoted violations with reason = "Grok 4.5 Cross Validation disagreed".
      • It clears safemodel_sex_nudity.positive to false.
    • 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.
  • task_safety_ptos_safemodel_sex_nudity.py was 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 = True when 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.
  • 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.
  • 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.
  • 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 reach build1FavIndex. Only the favoriteCount gate inside the function is gone.
    • However, a Fav event on an adult post with immersive video separately calls buildMMEmbMetadataDump (same file L438-442).
  • 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 buildMMEmbMetadataDump also removed the same favoriteCount >= 1 gate, 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_race counter 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.

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_written migration helper, and the v2_mirrors_v1_across_tier_matrix test.
  • 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 new 1fav_uec_count_race counter 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_RULES and 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 in PROMPT_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_race counter. 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

  1. The Relational Staleness Filter judges supersession and uses a pairwise classification framework verified (same section).
    • For a pair of a candidate item d_t and a topic-related item d' that arrived after it, a learned function f(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.
  2. 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.
  3. 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.

  1. It becomes an axis of comparison with X's stale-post handling.
    • The IS_STALE_POST14D bool 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.
  2. 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/.
  3. 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