0x00 한

Research / Daily reports

  • Sources read: 1
  • Canon files updated: 1
  • Radar entries: 1
In one lineX's visibility-filtering safety filter raised its backend lookup timeout to 390 ms and now evaluates author-based rules with a cached author id even when the tweet lookup fails.

1Safety filter waits longer on backends and caches authors for failed lookups — X

Source xai-org/x-algorithm@fee1d0f — public source code, 2026-09-10, 33 files, +1587/−391. These notes number X commits after the Gen4 release in sequence; this commit (2026-09-10, fee1d0f) is Gen4+19.

Background Since the Gen4 release on 2026-08-13, X has mirrored the For You pipeline source code to xai-org/x-algorithm about twice a day. visibility-filtering (VF, code in visibility-filtering/) is the gRPC safety filter service of the X home timeline and a Gen3 stack rewritten in Rust. When home-mixer (the service that assembles the For You feed) builds the candidate list, VF fetches tweet metadata and author labels for each candidate from two backends, fills them in, and feeds them to the rule engine. This commit is a medium-sized diff focused on VF; it fixes the time budget and failure handling of the backend lookups and shrinks the author label data structure.

The step in which VF fetches data from the backends and fills it in is called hydration. Tweet metadata comes from TES (Tweet Entity Service, X's core tweet lookup service). Author labels come from Gizmoduck (the user profile service). Hydration involves three intertwined decisions: how long to wait for the two backends, how far to proceed with verdicts when a lookup fails, and where latency is spent. This commit touched all three.

The changes fall on two main axes.

  • Axis 1 is the hydration time budget and failure handling. The wait time was increased, a path was opened to evaluate author-based rules from the cache even on failure, and recording of where latency is spent began.
  • Axis 2 is author label reduction and preparation of a cache primitive. Only the 11 labels the rules use remain, and the new cache is not yet used.
  • There are also side changes outside axis 2. The proto constants and model code of the recommendation model, an ads log field, a batch job of the label transparency tool, and the image builder changed.

What we learnedA. Unified the wait time for both backends into one 390 ms constant, raising it substantially

  • VF's hydration timeout was replaced by one unified constant, and the timeout of Strato (the X internal system that code goes through to look up or store data) calls rose from 100 ms for TES and 80 ms for Gizmoduck to 390 ms verified — visibility-filtering/hydration/mod.rs

  • This constant is used in two layers. The first is the hydrator layer. The client timeout of the TES and Gizmoduck hydrators had been a local constant of 150 ms each since the Gen4 release; both now reference this constant verified (same commit).

    • The CLIENT_TIMEOUT of the two hydrators was previously a local constant of 150 ms each, and now both reference crate::hydration::HYDRATION_TIMEOUT.
  • The second is the Strato client layer. The timeout constants hardcoded per service were both replaced by the value of this constant verified (same commit).

    • Before: TES 100 ms (TES_STRATO_REQUEST_TIMEOUT_MS: u64 = 100), Gizmoduck 80 ms (GIZMODUCK_STRATO_REQUEST_TIMEOUT_MS: u64 = 80)
    • After: both HYDRATION_TIMEOUT.as_millis() as u64
  • That is, the timeout that Strato callers actually wait for rose 3.9× and 4.9× for the two services respectively verified (same commit). It is a decision to give the VF service a much more generous latency budget for hydration.

  • Timeout-induced disagreements were possibly observed in the verdict-comparison canary introduced by Gen4+7 (reference_compare.rs, 2026-08-24) inferred, and this increase reads as the response to that.

What we learnedB. Even when the TES lookup fails, author-based rules are evaluated with the author id left in the cache

  • A tweet whose TES lookup fails can now also have its author found through the author id previously recorded in the cache; this fallback is on by default and the default capacity is 1 M entries verified — visibility-filtering/hydration/tes_hydrator.rs (+234/−42)

  • Previously, when a tweet was not found in TES or the lookup failed and the request also carried no author id, that candidate skipped verdict evaluation entirely verified (same commit).

    • A TES lookup of one tweet has three outcomes: Found(core_data), NotFound, Failed(error).
    • In the earlier code, if the result was not Found, there was no PureCoreData for that tweet. If resolve_candidate then could not find raw.request_author_id (the author id home-mixer put in the request, which may be absent), that candidate dropped out of the candidate list altogether.
  • This commit reinforced that case with a cache fallback. An author-id fallback cache field was added to TesHydrator verified (same commit).

    • New field definition:

      pub(crate) type AuthorIdFallbackCache = FallbackCache<TweetId, u64>;
      
      pub struct TesHydrator {
          pub tes_client: Arc<dyn TESClient + Send + Sync>,
          author_id_cache: Option<AuthorIdFallbackCache>,
      }
      
  • fetch_pure_core now returns a struct holding both the core data and the author ids recovered from the cache, instead of a HashMap<TweetId, PureCoreData> verified (same commit).

    • New return type:

      pub(crate) struct PureCoreHydration {
          pub(crate) core: HashMap<TweetId, PureCoreData>,
          pub(crate) recovered_authors: HashMap<TweetId, u64>,
      }
      
  • If the lookup result is Found, it records (tweet_id, author_id) in the cache and also fills in the core. For NotFound and Failed, it looks up the previous author_id of this tweet in the cache verified (same commit).

  • The priority in which resolve_candidate picks the author id is request, TES core, then cache verified (same commit).

    • If the request carries the author id directly, that is used; otherwise it is taken from the TES core if present; otherwise the cache fallback is used. If no author id is obtained from any of the three paths, that candidate skips verdict evaluation.

    • Code:

      let author = raw
          .request_author_id
          .or_else(|| core.get(&raw.tweet_id).map(|c| c.author_id))
          .or_else(|| recovered_authors.get(&raw.tweet_id).copied())?;
      
  • As a result, even if TES is down or does not respond, author-side rules are still evaluated as long as the previous author_id of this tweet is in the cache verified (same commit).

    • Author-side rules are the user's account state (suspended/deactivated/protected/erased/offboarded) and the 11 author user_labels. The 11 are covered in C below.
  • Tweet-side rules cannot be evaluated without the TES core, so they are still skipped verified (same commit).

    • Tweet-side rules are nullcast, community-only, NSFW media, takedown reason, exclusive content, and edit control.
  • The fallback settings are exposed through two new environment variables, and when the variables are not set the default is on verified (same commit). In a production deployment it can only be turned off by opt-out, and it is on from the moment of deployment.

    • Using is_none_or, it is on if the environment variable is absent, and if a value is present, that value is parsed.

    • Code that reads the two environment variables:

      pub(crate) fn author_id_fallback_enabled() -> bool {
          std::env::var("VF_AUTHOR_ID_FALLBACK_ENABLED")
              .ok()
              .is_none_or(|value| parse_env_flag(Some(&value)))
      }
      
      pub(crate) fn author_id_fallback_capacity() -> usize {
          std::env::var("VF_AUTHOR_ID_FALLBACK_CAPACITY")
              .ok()
              .and_then(|value| value.trim().parse::<usize>().ok())
              .filter(|capacity| *capacity > 0)
              .unwrap_or(1_000_000)
      }
      
  • At initialization, server_deps.rs leaves these two settings as info! fields verified (same commit). This log is captured per metric in the boot log, so if the actual production values differ, it is detected immediately.

    • Initialization log:

      info!(
          hydrator_count = 5,
          fallback_cache_enabled,
          author_id_fallback_enabled,
          author_id_fallback_capacity,
          home_rule_count,
          recommendations_rule_count,
          "VFServer initialized with prod clients"
      );
      

A and B are effectively one project. While A allows hydration to take longer by raising the timeout 2.6–4.9×, B builds an automatic fallback path for the case where it still fails.

This fallback does not reverse the "TesHydrator media fallback cache removed wholesale" observed in Gen4+9 (2026-08-26). The media fallback cached and reused the media information TES returned; this author-id fallback revives the previous value only for tweets TES could not respond for, and makes a partial verdict that evaluates only the author-side rules. In other words, it is the revival of a partial verdict, not the reuse of a result.

This change reads as narrowing the remaining robustness gaps after the large VF hydration simplification in Gen4+15 (2026-09-04) thinned the code by a net reduction of 800+ lines.

What we learnedC. Changed author labels to a data structure that holds only the 11 labels the rules actually use

  • The data structure that holds author labels in VF changed from a hash set to an enum representing only 11 labels plus a 64-bit bitset verified — visibility-filtering/models/author.rs (+49/−13)

  • This reduction is correct because the rules reference only these 11 labels. However, other labels coming from thrift are now explicitly discarded at the input stage and only counted by a counter.

  • models/author.rs was rewritten entirely verified (same commit).

    • Previous definition:

      use std::collections::HashSet;
      use xai_x_thrift::user_labels::LabelValue;
      
      #[derive(Clone, Debug, Default)]
      pub struct AuthorFeatures {
          pub is_suspended: bool,
          // ...
          pub user_labels: UserLabelSet,
      }
      
      #[derive(Clone, Debug, Default)]
      pub struct UserLabelSet {
          labels: HashSet<LabelValue>,
      }
      
    • New definition:

      #[derive(Clone, Copy, Debug, Default)]
      pub struct AuthorFeatures {
          pub is_suspended: bool,
          // ...
          pub user_labels: AuthorLabelSet,
      }
      
      #[derive(Clone, Copy, Debug, PartialEq, Eq)]
      #[repr(u8)]
      pub enum AuthorLabel {
          NsfwHighRecall,
          NsfwHighPrecision,
          NsfwNearPerfect,
          NsfwAvatarImage,
          NsfwBannerImage,
          SpamHighRecall,
          AbusiveHighRecall,
          Compromised,
          ReadOnly,
          ImpersonationHighPrecision,
          DoNotAmplify,
      }
      
      #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
      pub struct AuthorLabelSet(u64);
      
  • AuthorLabelSet(u64) is a single integer mapping the 11 variants to individual bits, and insert and has are handled with shift and AND operations verified (same commit).

  • Two new asserts pin the size. AuthorFeatures is exactly 16 bytes, and Option<AuthorFeatures> is also 16 bytes verified (same commit).

    • One bool field is used as the Option niche.

    • Asserts that pin the size:

      assert_copy::<AuthorFeatures>();
      assert_eq!(std::mem::size_of::<AuthorFeatures>(), 16);
      assert_eq!(std::mem::size_of::<Option<AuthorFeatures>>(), 16);
      
    • In the earlier HashSet<LabelValue> days, a heap allocation and hash buckets had to be taken every time.

  • The label mapping of the Gizmoduck hydrator shrank to a single explicit function verified (same commit). Each LabelValue coming from thrift (an enum of 20+ kinds) passes through this mapping and is narrowed to Option<AuthorLabel>, and if it is None, only a count is incremented.

    • Mapping function:

      fn author_label(value: LabelValue) -> Option<AuthorLabel> {
          match value {
              LabelValue::NSFW_HIGH_RECALL => Some(AuthorLabel::NsfwHighRecall),
              LabelValue::NSFW_HIGH_PRECISION => Some(AuthorLabel::NsfwHighPrecision),
              LabelValue::NSFW_NEAR_PERFECT => Some(AuthorLabel::NsfwNearPerfect),
              LabelValue::NSFW_AVATAR_IMAGE => Some(AuthorLabel::NsfwAvatarImage),
              LabelValue::NSFW_BANNER_IMAGE => Some(AuthorLabel::NsfwBannerImage),
              LabelValue::SPAM_HIGH_RECALL => Some(AuthorLabel::SpamHighRecall),
              LabelValue::ABUSIVE_HIGH_RECALL => Some(AuthorLabel::AbusiveHighRecall),
              LabelValue::COMPROMISED => Some(AuthorLabel::Compromised),
              LabelValue::READ_ONLY => Some(AuthorLabel::ReadOnly),
              LabelValue::IMPERSONATION_HIGH_PRECISION => Some(AuthorLabel::ImpersonationHighPrecision),
              LabelValue::DO_NOT_AMPLIFY => Some(AuthorLabel::DoNotAmplify),
              _ => None,
          }
      }
      
  • A new metric vf_author_labels{result="mapped"|"unmapped"} was added, along with a test that enforces at compile time that the contract does not drift verified (same commit).

    • Test:

      #[test]
      fn dashboard_generator_pins_the_author_labels_metric_name() {
          // ... asserts dashboard.py contains AUTHOR_LABELS_METRIC = "vf_author_labels"
      }
      
  • These 11 are exactly 1:1 with the 11 OON_USER_LABEL_DROPS rules folded into the canon in Gen4+13 (7ba7768, 2026-09-01) verified (same commit). That is, only the labels actually referenced by the rules remain, and the rest were removed from the input.

⚠ Re-checked the canon content. The safety pipeline label list in services/x.md already contained these 11 before. What this commit did was fix that fact into a data structure; it did not add new labels. However, the fact that other LabelValues defined in thrift (for example NSFW_TEXT_HIGH_PRECISION, NSFW_TEXT_HIGH_RECALL, NSFW_NEW_USER) are explicitly discarded at the hydrator layer is confirmed for the first time by this commit.

What we learnedD. VF now records, for the first time, whether it responded within the caller's time budget

  • VF now records as a metric whether it responded within the gRPC deadline carried in the request, and records request time split into two phases verified — visibility-filtering/filter_tweets.rs (+42)

  • filter_tweets.rs::FilterTweetsEndpoint::filter_tweets parses the standard gRPC grpc-timeout header in the request metadata verified (same commit).

    • It supports the unit suffixes H/M/S/m/u/n (hours, minutes, seconds, milli, micro, nano) of the gRPC spec. The number must be at most 8 digits, and any other format yields None.

    • Parsing function:

      fn parse_grpc_timeout(metadata: &MetadataMap) -> Option<Duration> {
          let raw = metadata.get("grpc-timeout")?.to_str().ok()?;
          let (digits, unit) = raw.split_at(raw.len().checked_sub(1)?);
          if digits.is_empty() || digits.len() > 8 || !digits.bytes().all(|b| b.is_ascii_digit()) {
              return None;
          }
          let value: u64 = digits.parse().ok()?;
          let nanos_per_unit = match unit {
              "H" => 3_600_000_000_000,
              "M" => 60_000_000_000,
              "S" => 1_000_000_000,
              "m" => 1_000_000,
              "u" => 1_000,
              "n" => 1,
              _ => return None,
          };
          Some(Duration::from_nanos(value.checked_mul(nanos_per_unit)?))
      }
      
  • At the point of a successful response, RequestMetricsGuard::record_deadline(grpc_timeout) is called verified (same commit). Counters count whether the response was within the deadline, exceeded it, or there was no deadline, and histograms record the remaining time and the overrun time.

    • Recording function:

      pub(crate) fn record_deadline(&self, grpc_timeout: Option<Duration>) {
          let Some(deadline) = grpc_timeout else {
              incr(DEADLINE, &[("outcome", "absent")], 1);
              return;
          };
          let elapsed = self.start.elapsed();
          if elapsed <= deadline {
              incr(DEADLINE, &[("outcome", "within")], 1);
              observe_vm(DEADLINE_REMAINING_MS, &[], millis(deadline - elapsed));
          } else {
              incr(DEADLINE, &[("outcome", "overrun")], 1);
              observe_vm(DEADLINE_OVERRUN_MS, &[], millis(elapsed - deadline));
          }
      }
      
    • The three counters are filter_tweets_deadline{outcome=within|overrun|absent}, and the two histograms are filter_tweets_deadline_remaining_ms / filter_tweets_deadline_overrun_ms.

  • VF now observes for the first time how much slack its own latency had against the deadline given by the caller (home-mixer), whether it overran, or whether it could not be measured verified (same commit).

  • filter.rs::FilterTweets::run measures request time in two phases verified (same commit). With the new histogram filter_tweets_phase_ms{stage="hydration"|"post_hydration"}, it is also possible to observe for the first time how total request latency is allocated between hydration and the rule engine.

    • Code that measures the two phases:

      let started = Instant::now();
      let hydration = self.hydration_pipeline.hydrate(...).await;
      let hydrated_at = Instant::now();
      ft_metrics::record_phase("hydration", hydrated_at - started);
      // ... rule engine
      ft_metrics::record_phase("post_hydration", hydrated_at.elapsed());
      

It is natural for A's timeout increase and this added observation to ship together. Once the timeout is raised, one needs to see how much it actually rose, and to know in which phase that latency is spent.

Also, the convention so far that home-mixer puts the standard gRPC grpc-timeout header in the request when calling VF is confirmed in code for the first time in this commit. In earlier commits the timeout was enforced only on the caller side, and the VF service did not read that value.

What we learnedE. A new sharded cache primitive arrived, but nothing uses it yet

  • A new 32-way set-associative × 64-shard CLOCK cache primitive arrived in VF, but it is not yet connected anywhere verified — visibility-filtering/clock_cache.rs

    • Other file changed with it: visibility-filtering/lib.rs
    • visibility-filtering/clock_cache.rs is a new file of 446 lines, registered in visibility-filtering/lib.rs with the single line pub(crate) mod clock_cache;. No code in this commit references this file.
  • The 32-way × 64-shard cache that arrived as the new file clock_cache.rs (446 lines) carries a dead_code attribute that suppresses the compiler warning verified (same commit). From this we infer it has no consumer yet, and which hydrator consumes the cache in clock_cache.rs in the next commit is the thing to watch.

    • File header:

      #![cfg_attr(not(test), expect(dead_code, reason = "TODO: no consumer wired yet"))]
      
    • It tells the compiler explicitly: "In non-test builds, we know this module is dead; do not emit a warning; the reason is that no consumer is attached yet."

  • It takes capacity capacity as an argument, creates set_count = capacity / WAYS sets, and distributes them across 64 shards verified (same commit). One shard holds several sets, and one set holds 32 slots (ways).

    • The value stored in a slot is the tuple (key: u64, generation: u64, value: Option<V>).

    • Main definitions:

      const WAYS: usize = 32;
      const SHARDS: usize = 64;
      
      #[derive(Clone, Copy, Debug, PartialEq, Eq)]
      pub(crate) enum Lookup<V> {
          Miss,      // key is not even registered in this cache
          NotFound,  // registered but the value is None (e.g., the backend returned NotFound)
          Found(V),
      }
      
      pub(crate) struct ClockCache<V> { /* shards, set_count, next_generation */ }
      
  • The CLOCK algorithm runs as follows verified (same commit).

    • Insert (insert(generation, key, value)): if the same key already exists and the stored generation is larger, it is skipped. This is to keep an old value from overwriting a new one. Otherwise it overwrites in place and marks it Mark::Referenced. If the key is absent, the hand rotates, decaying Referenced → Cold, and inserts at the first Cold or Empty slot it meets.
    • Lookup (get(key)): if it finds the same key, it promotes it to Mark::Referenced and returns Lookup::Found(value). If absent it is Miss, and if the stored value is None it is NotFound.
    • Generation: for each request, begin_request() -> u64 assigns a monotonic value. This value decides whether a later request may overwrite an older cache value.
  • This cache is a primitive separate from the existing FallbackCache verified (same commit).

    • The existing FallbackCache is hydration/fallback_cache.rs, folded into the canon in Gen4, and is a TinyLFU based on moka::sync::Cache protected by a single mutex.
  • The two caches differ in locking unit, replacement policy, slot layout, value type, and concurrency verified (same commit).

Axis Existing FallbackCache (moka and Mutex) New ClockCache
Locking unit One Mutex for the whole cache One Mutex per shard (64)
Replacement policy TinyLFU (probabilistic frequency) CLOCK (Referenced ↔ Cold decay)
Slot layout Dynamic hash map Fixed 32-way set-associative array
Value type Arbitrary (Clone) Copy constraint (suited to values around the size of u64)
Concurrency Can bottleneck on one mutex Mostly non-blocking with 64-way sharding

That is, it reads as a new hot-path cache that is far more resistant to write throughput and lock contention. The main thing to watch is which hydrator (TES author_id? Gizmoduck AuthorFeatures? SafetyLabel expiring cache?) it connects to as a backend in the next commit.

What we learnedF. In the Phoenix ranker proto, two surface constants and one behavior aggregation type got names

  • In the ranking wire proto of Phoenix (the Grok-based Transformer ranker), surface constants for Trend and Explore Spotlight and an aggregation constant for long-dwell immersive behavior were newly added verified — phoenix/crates/serving/xai-recsys-proto/proto/recsys.proto

    • Other file changed with it: phoenix/python/common/xai-proto/proto/recsys.proto
    • The new constants are the two surface constants TREND_PAGE and EXPLORE_SPOTLIGHT and the action aggregation constant IMMERSIVE_LONG_DWELL.
    • As enum values, they are two ProductSurface values (TREND_PAGE=16, EXPLORE_SPOTLIGHT=17) and UserActionAggregationType::IMMERSIVE_LONG_DWELL=29.
  • Numbers 16 and 17 of the ProductSurface enum got the names of the Trend surface and the Explore Spotlight surface respectively (previously PLACEHOLDER_16 and PLACEHOLDER_17) verified (same commit). The two surfaces were added after the 15 constants folded into the canon in Gen4+3.

    • Changed enum:

      enum ProductSurface {
        // ... 1-15 as before
        PRODUCT_SURFACE_HOME_TIMELINE_RANKED_FOLLOWING = 15;
        PRODUCT_SURFACE_TREND_PAGE = 16;
        PRODUCT_SURFACE_EXPLORE_SPOTLIGHT = 17;
        PRODUCT_SURFACE_PLACE_HOLDER_18 = 18;
        PRODUCT_SURFACE_PLACE_HOLDER_19 = 19;
        PRODUCT_SURFACE_PLACE_HOLDER_20 = 20;
      }
      
  • There is no actual home-mixer connection yet, so no code in this commit puts these two constants into Phoenix for ranking verified (same commit). For now it is only a signal that the proto slots are ready.

  • In UserActionAggregationType, the previous PLACEHOLDER_29 got the name IMMERSIVE_LONG_DWELL = 29 verified (same commit).

    • Changed enum:

      enum UserActionAggregationType {
        // ...
        IMMERSIVE = 26;
        ADS_CHAD_WITH_CONV_TYPES = 27;
        ADS_WITH_KV_AD_HISTORY = 28;
        IMMERSIVE_LONG_DWELL = 29;
        PLACEHOLDER_30 = 30;
        // ...
      }
      
  • Separately from the existing IMMERSIVE = 26 (aggregation of interaction sequences on the immersive surface), immersive interactions with long dwell time can now be split into a separate aggregation type verified (same commit).

  • Which training config consumes this new aggregation does not appear in this commit verified (same commit).

What we learnedG. The user tower of the two-tower model now uses a separate segment ID for padding positions

  • The user tower of the two-tower model (a retrieval model structure that embeds the user side and the candidate side separately) was changed to use a separate segment ID for padding positions verified — phoenix/xrex/models/recsys_two_tower_model.py (+15/−4)

    • It uses PADDING_SEGMENT_ID for padding positions and HISTORY_SEGMENT_ID for real history positions. It changed from the earlier all-zero to one that reflects the mask.
  • The earlier user_tower_segment_ids logic had two branches verified (same commit).

    • If use_history_segment_ids=True, it filled the whole array with HISTORY_SEGMENT_ID. This is an inaccurate approach that treats even padding as the same segment as history.

    • If False, it treated every position as 0 with jnp.zeros. RoPE (rotary position encoding) and attention bias were computed even at padding positions.

    • Earlier code:

      def user_tower_segment_ids(batch: int, seq_len: int, *, use_history_segment_ids: bool) -> jax.Array:
          if use_history_segment_ids:
              return jnp.full((batch, seq_len), HISTORY_SEGMENT_ID, dtype=jnp.int32)
          return jnp.zeros((batch, seq_len), dtype=jnp.int32)
      
  • When padding_mask is passed in, the new logic mixes HISTORY_SEGMENT_ID and PADDING_SEGMENT_ID according to the mask value verified (same commit).

    • Later code:

      def user_tower_segment_ids(
          batch: int,
          seq_len: int,
          *,
          use_history_segment_ids: bool,
          padding_mask: jax.Array | None = None,
      ) -> jax.Array:
          if use_history_segment_ids:
              return jnp.full((batch, seq_len), HISTORY_SEGMENT_ID, dtype=jnp.int32)
          if padding_mask is None:
              return jnp.zeros((batch, seq_len), dtype=jnp.int32)
          return jnp.where(padding_mask, HISTORY_SEGMENT_ID, PADDING_SEGMENT_ID).astype(jnp.int32)
      
    • Call site:

      user_segment_ids = user_tower_segment_ids(
          B, T,
          use_history_segment_ids=self.config.use_history_segment_ids,
          padding_mask=user_padding_mask,
      )
      
  • Now the user tower's attention, RoPE, and attention bias are computed so that padding positions are distinguished from real history verified (same commit). It is a correctness fix in the same direction as "native support for variable-length history in the FA4 attention kernel" folded into the canon in Gen4+15 (2026-09-04, 9b0dc31).

What we learnedH. The ads injection log now carries an ads blender label

  • A field that records the ads blender label was added to home-mixer's ads injection log verified — home-mixer/side_effects/ads_injection_logging_side_effect.rs (+5/−1)

    • The field name is ads_blender, and the diff is short:

      ads_blender: match query.request_type {
          RequestType::Following => "following".to_string(),
          _ => query.params.get(AdsBlenderType),
      },
      
  • The ads_blender value attached to the ads injection log payload is determined by request type. For RequestType::Following it uses the hardcoded "following", and only on other surfaces does it look up the AdsBlenderType feature-switch param verified (same commit).

  • Following the five ads blenders introduced by Gen4+10 (2026-08-28, 24c6094), a separate blender label "following" for the Following surface formally enters the logging axis verified (same commit).

    • The five blenders are safe_gap, partition_organic, partition_organic_low_risk, time_gap, and multi_risk.
  • Whether this is a sixth blender, or the Following surface goes through a separate pipeline outside the five blenders so that only the label is captured in logging, cannot be confirmed from this diff alone inferred. The latter fits the code flow better inferred. Skipping the param lookup entirely reads as a sign of different pipelines.

What we learnedI. The label transparency tool (UTH) gets a daily per-country account takedown batch, and label aggregates get a country field

  • A daily batch job for per-country account takedowns was newly added to the Under-The-Hood (UTH, X's label transparency tool) data pipeline verified — under-the-hood/scalding/UthPctdAccountTakedownEventsJob.scala

    • The job name is UthPctdAccountTakedownEventsJob, and the new batch job is 143 lines.
  • A countryCodes field was added to the UTH label aggregate verified — under-the-hood/thrift/uth_serving.thrift

  • under-the-hood/ is the backend of the transparency tool for users' personal account and post labels that X exposes at x.com/i/under_the_hood verified (same commit).

  • The batch job joins two sources verified (same commit).

    • AuditServicePctdActionsScalaDataset is the PCTD (post and content takedown) action log. It keeps only entries that are of account type, were successfully taken down, and have a country: .filter { a => a.type == PctdActionType.User && a.takendown && a.countryCode.nonEmpty }.
    • UsersourceScalaDataset is user.takedowns.takedownCountryReasons of the user snapshot. The reasons are converted as TakedownReason::LegalRequest → "LEGAL_REQUEST", BystanderReport → "BYSTANDER_REPORT", Dmca → "DMCA" (country hardcoded as "xy", no country information), and UnspecifiedReason → "UNSPECIFIED".
  • If several reasons are captured for the same (userId, country), the one with the lower priority value is kept verified (same commit).

    • Priority table:

      private val ReasonPriority = Map(
        "LEGAL_REQUEST" -> 0,
        "BYSTANDER_REPORT" -> 1,
        "DMCA" -> 2,
        "UNSPECIFIED" -> 3
      )
      
  • The result is written as UthAccountTakedownEvent(userId, day, reason, country) to a partitioned parquet at daily_account_takedowns/YYYY/MM/DD verified (same commit).

    • Default reducers are 50, default usersourceSnapshotMaxAgeDays is 10, and cross-DC snapshot access (AllowCrossDC) is allowed.
    • Test accounts are excluded by the test user filter (TestUserFilter.isTestUserId).
  • At the same time, two structs in uth_serving.thrift gain a country field verified (same commit).

    • Changed structs:

      struct UthPostLabelAggregate {
        1: optional string label (personalDataType = 'TweetSafetyLabels')
        2: optional list<UthDayCarriedRemoved> days
        3: optional list<string> countryCodes  // NEW
      }
      
      struct UthAccountLabelAggregate {
        1: optional string label (personalDataType = 'UserSafetyLabels')
        2: optional list<i32> activeDays
        3: optional list<string> countryCodes  // NEW
      }
      
  • In sum, the UTH transparency tool gains two new things verified (same commit).

    • It now has its own dataset of daily event logs of when, in which country, and for which reason each account was taken down.
    • It is ready to expose a country code list in the label aggregate.
  • This change pairs with Gen4+12 (2026-09-01, 6384ca7), where the 16 countries subject to NSFW gating were stated explicitly in code for the first time. It reads as a plan to let users see in UTH which label was active in which country. We are watching whether per-country filters or displays appear on the UTH screen.

What we learnedOther changes

  • The Safety-label expiring cache reserves the capacity to use in advance at initialization, avoiding reallocation on the first insert verified — visibility-filtering/safety_label_source/expiring_cache.rs
    • ExpiringCache::with_clock calls cache.reserve(capacity) at initialization.
  • The dead keyed-result path was removed from the hydration metrics code, and a new author label aggregation helper was added verified — visibility-filtering/hydration/metrics.rs
    • hydration/metrics.rs +21/−154: timed_keyed_rpc, KeyedResultCounts::from_results, and outer_timeout were removed.
    • After the batch unification in Gen4+15 (9b0dc31, 2026-09-04), this keyed-result path was dead code.
    • The new helper is record_author_labels(mapped, unmapped).
  • The motion-reveal thumbnail generation in grox (the Grok-based nearline classifier sub-repo) was refined to take more frames and pass them through alignment, denoise, and color verification stage by stage verified — grox/libs/video_tools/image.py (+288/−25)
    • Thumbnail generation creates a single-frame highlight from a video. Two stages, ECC affine alignment and Farneback optical flow refinement, were added to the motion-reveal image builder (the video → highlight frame generator) that handles this.
    • In sum, the changes are a 32 → 48 frame cap, ECC affine alignment, Farneback optical flow refinement, weighted median, and an LAB skin color plausibility gate.
    • _MOTION_REVEAL_MAX_INPUT_FRAMES changed from 32 to 48, and six new helpers were added.
      • _ecc_align: aligns the affine transform with cv2.findTransformECC.
      • _is_static: decides whether a frame is static with cv2.phaseCorrelate. A frame is static if both the shift magnitude and the phase correlation response are at or below the threshold.
      • _dense_align_residual: finely aligns the residual with Farneback dense optical flow.
      • _weighted_median: computes the per-pixel weighted median of the overlay shifted after alignment.
      • _native_chroma: computes the median chroma coordinate of the brightest 30% of pixels in the LAB color space.
      • _skin_plausible: a gate that checks whether the overlay's chroma median is within a distance of 12 on the skin-color axis. If false, it prevents the false positive of a skin overlay.
    • The PlanSafetyPtosSpecialVideo DAG, which was folded into the canon in Gen4+12 (2026-09-01), consumes these motion-reveal frames, so this refinement likely directly affects the verdict quality of that DAG inferred.
  • The feature-switch defaults mirror in home-mixer/params/param.rs has no substantive change other than its timestamp being updated from 2026-09-09T16:23:17Z to 2026-09-10T16:21:03Z.

"pub(crate) const HYDRATION_TIMEOUT: Duration = Duration::from_millis(390);" — visibility-filtering/hydration/mod.rs:28, 2026-09-10

"let author = raw.request_author_id.or_else(|| core.get(&raw.tweet_id).map(|c| c.author_id)).or_else(|| recovered_authors.get(&raw.tweet_id).copied())?;" — visibility-filtering/models/mod.rs:45-49, 2026-09-10

Canon update In services/x.md, a new ### Gen4+19 subsection was created under the ## Safety section listing ten axes, A–J, and the last updated date at the top of the document was moved from 2026-09-11 to 2026-09-12. B (AuthorId fallback) and D (gRPC deadline observation) are new data points for the safety pipeline, so they were explicitly folded into the canon. F (TREND_PAGE, EXPLORE_SPOTLIGHT, IMMERSIVE_LONG_DWELL) and G (padding segment ID) are changes to the ranking wire and training configuration, so they were folded in as well. E (the clock_cache primitive) is not yet connected, but a consumer is expected to attach in the next commit, so it was documented explicitly.

Why it matters This commit raises the verdict success rate by extending the timeout and opens a partial-verdict path, gaining robustness through partial-open instead of fail-open (passing content through when a verdict fails). It continues the line of visibility-filtering robustness improvements that has run since the Gen4 release, and Gen4+19 adds a higher timeout budget, a fallback cache, a smaller label data structure, and phase observation on top of it.

The main milestones of this line so far are as follows:

  • Shadow deployment mirroring in Gen4+4 (2026-08-19, dark_traffic_setup.rs)
  • A verdict-comparison canary in Gen4+7 (2026-08-24, reference_compare.rs)
  • The rules restructuring in Gen4+13 (2026-09-01, 7ba7768)
  • The hydration simplification with a net reduction of 800 lines in Gen4+15 (2026-09-04, 9b0dc31)

⚠ Claim changed: none to update. This diff overturns no earlier canon statement, and caveat list #1 in the canon (this repository's canon document), that VF is fail-closed, is still valid. A tweet that could not be judged because hydration failed is still skipped, and the author fallback only turns what used to be skipped into a partial verdict.

Radar

Added one Kuaishou cascade recommendation paper to "New this month" in radar.md. The UniRec proposed in the paper is a method that aligns the fusion of the pre-ranking and ranking stages of a cascade recommender through joint learning of the two stages, and in the Kuaishou production A/B it raised app usage duration by +0.616% verified — arXiv 2609.11052, submitted 2026-09-10.

  • The problem it addresses is that the cascade stages of industrial recommendation (pre-ranking, ranking) are optimized separately under different objectives, feature spaces, and latency constraints verified (same paper). The upstream filters out items the downstream prefers, and the downstream fusion offsets upstream improvements.
  • The two fusion agents share part of the input embedding and are trained together in a single computation graph, so gradients flow between stages verified (same paper).
  • Dual-axis preference alignment works in two directions verified (same paper).
    • Vertical: transfers downstream pairwise preference to the upstream fusion score.
    • Horizontal: recasts dozens of pairwise objectives over heterogeneous prior signals as bidirectional preference evidence.
  • In addition, attribute group-relative regularization suppresses the side effect of unconstrained end-to-end fusion over-concentrating on particular attribute groups verified (same paper).
  • In a 21-day production A/B, app usage time showed a +0.616% relative gain, and the model was fully deployed on the Kuaishou platform verified (same paper).

What to check next