- Sources read: 1
- Canon files updated: 1
- Radar entries: 0
1Phoenix ranker's backbone and re-ranking head layers first appear in code — X
Source xai-org/x-algorithm@85ac72a — public source code, 2026-09-02 19:46 UTC, X.AI Corp. These notes number X commits in order after the Gen4 release; this commit is Gen4+14.
Background The Gen4 release (2026-08-13) revealed that Phoenix, X's ranking model, is a Transformer that outputs 24 engagement probabilities per document, and Gen4+11 (2026-08-28, bc8e5f0) added a rerankerHeadTag (re-ranking head tag) field to the response scores, which first suggested that "several re-ranking heads exist." In a multi-task ranker, backbone is the standard term for the shared encoder/tower, and its output splits into several task heads (re-ranking, diversity, slate, etc.). However, the proto response did not expose backbone-layer output, so the canon (this repository's canon notes) did not say what the layer below Phoenix's heads outputs. This commit (Gen4+14) exposes that backbone output field in the response and makes home-mixer (the server that assembles X's feed responses) request it for all five feeds.
What we learnedA. home-mixer requires backbone scores in all five feed requests
home-mixer started turning on the backbone-score request flag as a hardcoded value at all five feed endpoints verified —
home-mixer/server.rs.- The line that turns it on is
query.return_backbone_scores = true. - Each of the five gRPC endpoints turns on this flag right before handing the request to Phoenix.
- ScoredPostsService (internal scored posts)
- ForYouFeedService (For You main)
- The polling variant of ForYouFeed
- RankedFollowingFeedService
- FollowingFeedService
- There is no parameter gating. That is, the production default is on.
let RequestContext { b3_info, mut query, root_span, } = ctx; query.return_backbone_scores = true; let output = self.get_for_you_feed(query).instrument(root_span).await?;—
home-mixer/server.rs, 2026-09-02- The line that turns it on is
A new bool field that decides whether to request backbone scores was added to the request struct verified —
home-mixer/models/query.rs.- The struct is
ScoredPostsQuery, and the field isreturn_backbone_scores: bool. - The default constructor (
ScoredPostsQuery::new) initializes it tofalse, and only the server entry point sets it totrue. - In other words, it is off by default in internal pipeline and test paths, and only the five feed requests entering through the gRPC server require backbone scores.
- The struct is
The Phoenix request builder passes this flag through unchanged verified —
home-mixer/util/phoenix_request.rs.- One line,
return_backbone_scores: query.return_backbone_scores, carries the value from the request context to the proto field.
- One line,
What we learnedB. The Phoenix response carries backbone output separately, and a candidate carries scores from both layers
The Phoenix proto added backbone fields to both the request and the response verified —
phoenix/crates/serving/xai-recsys-proto/proto/recsys.proto.- One request field and two response fields form a set.
- Request:
returnBackboneScores(#20) - Response:
backboneTopLogProbs(#10,repeated float) - Response:
backboneContinuousValues(#11,repeated float)
- Request:
Topappears to refer to the top log probabilities of the discrete head (for example, top-K logits over the action vocabulary), andContinuousto the dense regression output inferred.- The evidence is the naming convention, and that it corresponds to
topLogProbs(the discrete values the reranker head outputs) already present inNextActionDistributioninferred. - The structure in which the reranker head is a fine-tuned head sitting on this backbone is consistent with the
rerankerHeadTagfield that the earlier canon version observed inferred.
message PredictNextActionsRequest { ... PageDecodeParams pageDecode = 19; bool returnBackboneScores = 20; } message NextActionDistribution { ... SlateContext slateContext = 9; repeated float backboneTopLogProbs = 10; repeated float backboneContinuousValues = 11; }- One request field and two response fields form a set.
A backbone score field was added to the candidate object, and the scorer parses the response in two layers and stores them verified —
home-mixer/models/candidate.rs,home-mixer/scorers/phoenix_scorer.rs.- The new field is PostCandidate's
backbone_scores: Option<PhoenixScores>. - The scorer parses the same prediction response twice.
candidate_scoresis the reranker head output (the existingphoenix_scores), andcandidate_backbone_scoresis the backbone output newly exposed this time. - In other words, a candidate leaves for the next stage carrying the scores of both layers from one prediction round.
// candidate.rs pub struct PostCandidate { ... pub reranker_head_tag: Option<u32>, #[serde(default, skip_serializing_if = "Option::is_none")] pub backbone_scores: Option<PhoenixScores>, ... } // phoenix_scorer.rs .map(|c| PostCandidate { phoenix_scores: predictions.candidate_scores(&c.get_original_tweet_id()), backbone_scores: predictions.candidate_backbone_scores(&c.get_original_tweet_id()), ... })- The new field is PostCandidate's
What we learnedC. This commit does not yet show where backbone scores are used
- The scorer only stores them, and the downstream consumers lie outside this diff inferred.
- However, the fact that the backbone output keeps being carried on the candidate means at least one of the following inferred. The evidence is that
backbone_scoresis a field ofPostCandidateand, judging by the scaffolding, no path drops the value.- The re-ranking and diversity stages consume backbone logits as additional features.
- The ads/feed mixer records backbone output as a payload for debugging/attribution.
- Another scorer uses backbone output as input, for example VMRanker's slate reordering or the ads blender MultiRiskAdsBlender.
- It is an observability channel that lets offline analysis and A/B verdicts see "how much the backbone moved, not the reranker head."
- New consumers are likely to appear in the next commits inferred.
What we learnedD. The default for Phoenix retrieval's user-history aggregation changed to long dwell
The default for how Phoenix retrieval aggregates user history changed from short dwell to long dwell verified —
home-mixer/params/param.rs.- The sync timestamp in the file header was updated from
2026-09-01T16:42:25Zto2026-09-02T16:03:45Z, so the production feature switch actually changed within a day and the source mirror caught up with it. - The comment at the top of the file reads: "mirrored from config feature-switch defaults; last sync ...".
- Long-dwell aggregation merges only the candidates the user stayed on for a long time into the history embedding, which biases retrieval's signal further "toward high-quality engagement" inferred. The evidence is that this is consistent with the trend since Gen4+6 (2026-08-21), when X greatly expanded the ads conversion head and shifted weight toward dwell-based positive signals.
param!( PhoenixRetrievalAggregationType, String, "rust_home_mixer_phoenix_retrieval_aggregation_type", "DENSE_WITH_LONG_DWELL" // was "DENSE_WITH_SHORT_DWELL" );- The sync timestamp in the file header was updated from
What we learnedOther changes
Phoenix serving's batch preparation code added a defense in two places that zero-initializes the buffer before filling it verified —
phoenix/crates/serving/xai-recsys-engine/src/python.rs.- The places initialized are the candidate embedding slice and the search-query embedding slice.
- Previously, values from the previous batch remained in the reused buffer, and when
copy_len < embedding_row.len()(the filled length is smaller than the slot size), the leftover bytes leaked. - A
copy_len > 0condition is also applied, so the copy is skipped when the length to copy is 0. The commit does not state a reason.- Both sides are sliced to the same
copy_len, so a zero-length copy does not panic either verified — Rustslice::copy_from_slicedocs. This function panics only when the two slice lengths differ.
- Both sides are sliced to the same
- As a follow-up to Gen4+11 (2026-08-28, bc8e5f0), which cleaned up the emb_table gRPC receive pipeline, it closes a potential correctness issue at the batch-reuse boundary.
.par_chunks(chunk_size) .zip(candidate_embeddings_slices.par_iter_mut()) .for_each(|(items_chunk, shard_slice)| { shard_slice.fill(f16::ZERO); shard_slice .par_chunks_exact_mut(row_size) ... }); ... if search_query_embedding_dim > 0 && let Some(sq_slice) = ... { sq_slice.fill(0.0); sq_slice .par_chunks_exact_mut(candidate_seq_len * search_query_embedding_dim) ... }
Canon update In the "Safety / visibility-filtering / labeling pipeline" section of services/x.md, a new subsection was created: "Gen4+14 (2026-09-02, 85ac72a) — home-mixer requests Phoenix backbone scores + scarecrow XReview intake introduced + VF rules Box<dyn> removed + checkpoint in-envelope derived master key". A Gen4+14 row was added to the "One-line summary" and the generation comparison header. The "Phoenix training and index" section now reflects the backbone scores response field and the xrecsys_two_tower hyperparameter changes. 85ac72a(2026-09-02, Gen4+14) was added to the commit sha list in "## Sources", along with 8 new citations of related files. Last updated was raised from 2026-09-03 to 2026-09-04, and the Checked git log directly line was also changed from 2026-09-03 to 2026-09-04 and from 13 commits to 14.
Why it matters The layered structure of the Phoenix ranker is now confirmed in code.
- After the Gen4 release, the canon knew only the top-level semantics: "Phoenix is a Transformer that emits 24 probabilities per candidate." The
rerankerHeadTagfield in Gen4+11 let us observe that "several re-ranking heads exist," and Gen4+14 confirms that "beneath those heads there is a backbone layer whose output is a separate response field." This means the standard form of a multi-task ranker (a shared bottom and task-specific heads) is present in X production as is. return_backbone_scoresis forced on in the five feeds without parameter gating. This is a strong signal that this response addition is already served as a production default, not at an A/B experiment stage. It means the Phoenix serving side is ready to compute the backbone output and put it in the response, and that the latency budget has room.- "Carrying the backbone score on the candidate" is a reservation that lets the next layer consume it. This commit shows only the storage, but code in which downstream components such as MultiRiskAdsBlender, VMRanker, and DPP re-rank (re-ranking that reflects candidate diversity) use backbone logits as additional features is likely to follow soon. Paired with Gen4+11's
rerankerHeadTag, this makes it something to watch whether code for "switching, ensembling, and attribution among hydra heads" opens up in later commits. - That the Phoenix retrieval dwell default moved to long is consistent with the trend of expanding the ads conversion head since Gen4+6. In effect, in the two-layer ranker, the signal the backbone learns and the user-history aggregation retrieval consults are aligned in the same direction (long-dwell-heavy).
2scarecrow (formerly Botmaker) gained a path to file automatic reports with XReview — X
Source xai-org/x-algorithm@85ac72a — public source code, 2026-09-02, X.AI Corp. The related files are 3 new and 1 modified, 333 lines in total. By the numbering of these notes it is Gen4+14, and by content it is an item entering the canon for the first time.
Background scarecrow (formerly Botmaker) is X's internal "rule-based automatic detection and action" system. scarecrow is what X.AI has kept and expanded after the acquisition from the old Twitter's Botmaker (an engine that applies rules to traffic and takes actions), and the Scala files in this commit still carry the com.twitter.botmaker package legacy. XReview is the name of the report and moderation queue (X's internal name in 2025–2026); when a user presses "report" in the UI, the report flows to XReview and becomes a review target. Before this commit, the canon had no programmatic link between scarecrow and XReview. This commit opens a new gRPC pipeline through which scarecrow's automatic detection bots programmatically put reports into XReview "as if a user had reported them."
What we learnedA. scarecrow rules file reports with XReview through two functions
Two new function units send reports from scarecrow to XReview verified —
scarecrow/legacy/XReviewIntakeRegistry.scala(179 lines, new).- The two functions are
CreateXReviewReportProdandCreateXReviewReportStaging. They can be called from scarecrow's rule DSL and send reports to production and staging XReview respectively. - A scarecrow rule passes seven arguments when it calls this function.
entityType: "post" or "profile"entityId: tweet id or user iduserId: the reported userreportType: an XReview allowlist value (for example,bystander_impersonation)botId: the automatic detection bot that created this report- Optional
note - Optional
victimId: the victim's user id
- The self-description attached to the CreateXReviewReportProd class is the same as the quote below.
"Submits a report to production XReview Intake over gRPC. entityType must be post or profile. reportType must be an XReview-allowlisted value. Impersonation bots should use bystander_impersonation (lane tags key on that type, not generic impersonation). Each evaluation includes detection_timestamp_ms so intake's content-hash report id is unique; evidence rollup then applies." —
XReviewIntakeRegistry.scala,descriptionof the CreateXReviewReportProd class- The two functions are
The XReview intake was registered in the downstream services list verified —
scarecrow/legacy/DownstreamServices.scala.- The registered name is
XReviewIntake.
case object XReviewIntake extends DownstreamService { override val name = "XREVIEW_INTAKE" }- The registered name is
A separate client exists for staging, and the staging function is marked as experimental with no real action verified —
XReviewIntakeRegistry.scala.CreateXReviewReportStagingsets the action level toNO_ACTION.- In scarecrow, the action level is metadata that indicates whether this function is a real action or a dry run. The staging version is marked
NO_ACTION, which states explicitly that it is experimental and the rule is not applied in practice. - The production version is
PROD_AGENT_WORKFLOW_ACTION. The phrase "agent workflow" means it is part of an automatic action pipeline.
Developer local environments and personal-account instances do not call a real XReview server verified —
XReviewIntakeRegistry.scala.- In
localModeorpersonalAccountMode, it is replaced with a noop.
if (localMode() || personalAccountMode()) { XReviewReportSubmitter.noop(statsReceiver) } else { ... }- In
scarecrow connects to XReview intake over mTLS verified.
- The flag
xreview.tls.ca-certtakes the CA certificate path, andSharedXReviewIntakeClientModule.provideXReviewIntakeClientconsumes it to create the mTLS connection. - The client comes from a shared module in the
com.twitter.useng.common.xreviewpackage. useng is X's internal shared-library namespace. - In other words, this suggests that systems other than scarecrow also access XReview intake programmatically.
- The flag
What we learnedB. Bot reports enter through the same surface as user reports, tagged with automatic detection as the source
The report payload fills in XReview's SubmitReportRequest gRPC proto as is verified —
scarecrow/legacy/XReviewReportBuilder.scala(59 lines, new).- A few keys directly show the nature of this pipeline.
report_surface = "x_app": the report arrives through the same surface as user reports.report_source = "proactive": tells XReview that the report comes from automatic detection ("proactive"), not from a user report ("user"). In other words, XReview uses this field to distinguish which reports were created by a person's click and which were put in by a bot.subject = "Botmaker report": the top line of the report is fixed.reporter_id = "0": a bot report has no reporting user id. Instead,detection_bot_idholds which bot created the report.
val ReportSurfaceValue = "x_app" val ReportSourceValue = "proactive" val Subject = "Botmaker report" val ReporterIdValue = "0" ... val reportFields = Seq( pb(XReviewReportKeys.Subject, Subject), pb(XReviewReportKeys.ReportSurface, ReportSurfaceValue), pb(XReviewReportKeys.ReportType, reportType), pb(XReviewReportKeys.ReportSource, ReportSourceValue), pb(XReviewReportKeys.ReportedEntityType, entityType), pb(XReviewReportKeys.ReportedEntityId, entityId.toString), pb(XReviewReportKeys.ReportedUserId, userId.toString), pb(XReviewReportKeys.DetectionBotId, botId.toString), pb(XReviewReportKeys.DetectionTimestampMs, detectionTimestampMs.toString) ) ++ note.filter(_.nonEmpty).map(n => pb(DescriptionKey, n)).toSeq ++ victimId.filter(_ != null).map(id => pb(XReviewReportKeys.VictimUserId, id.toString)).toSeq- A few keys directly show the nature of this pipeline.
The entityType whitelist is limited to "post" and "profile" verified.
- The
normalizeEntityTypefunction throwsIllegalArgumentExceptionon any other value, which keeps scarecrow rules from putting arbitrary entity types into XReview.
- The
What we learnedC. XReview groups duplicate reports afterward, and the report type decides the review lane
- The uniqueness of a report id is determined by a content hash together with a timestamp verified.
- The quote above reads: "intake's content-hash report id is unique; evidence rollup then applies". It means the XReview server builds the report id from a content hash and
detection_timestamp_ms. - Even if the same bot detects the same content twice, the reports are separate if the timestamps differ, and XReview rolls up (aggregates) the evidence afterward.
- That is, duplicate detections naturally occur many times in bot reports, and XReview handles those duplicates.
- The quote above reads: "intake's content-hash report id is unique; evidence rollup then applies". It means the XReview server builds the report id from a content hash and
- Impersonation detection bots must use
bystander_impersonationverified.- The self-description reads: "Impersonation bots should use bystander_impersonation (lane tags key on that type, not generic impersonation)."
- The lane tag here appears to mean XReview queue routing (which human reviewer team and SLA a report goes to).
- That is, XReview has a "lane" concept that routes reports to reviewer teams, and the
report_typevalue decides the lane. - If a bot uses the wrong
report_type, the report is routed to the wrong lane, and review is delayed or ignored.
What we learnedOther changes
- There are five observability (metrics) counters verified —
XReviewReportSubmitter.scala(91 lines, new).attempted: number of callssubmitted: successesskipped: skipped because there is no prodClient/stagingClientunsupported_entity: entityType outside the whitelistrpc_failed: gRPC failure- Failures are logged at
warnand the exception is rethrown.
Canon update Under the "Safety / visibility-filtering / labeling pipeline" section of services/x.md, a new Gen4+14 subsection was created and grouped with the contents of section 1 above. The scarecrow → XReview intake pipeline is the first such item to enter the canon, so it was placed in a separate subsection, "scarecrow → XReview automatic report pipeline (newly released 2026-09-02)". The three files (XReviewIntakeRegistry, XReviewReportBuilder, XReviewReportSubmitter) and the modified DownstreamServices were added to "## Sources".
Why it matters X's moderation pipeline expanded with this commit.
- The safety layers the earlier canon version knew were two.
- visibility-filtering is the layer that drops candidates inline in the scoring and serving path or attaches an interstitial (an action that overlays a warning screen).
- grox PtOS is the content labeling layer in which the Grok LLM summarizes tweets and attaches labels.
- This commit (Gen4+14) exposes a third layer in code: scarecrow → XReview. Automatic detection bots push reports into the XReview queue through intake as if they were user reports, and XReview then handles evidence rollup and routing to human reviewers (lane tag). This is an asynchronous report and aggregation pipeline, not an inline drop.
- The
report_source = "proactive"field suggests that user reports ("user") and bot reports ("proactive") enter XReview in the same queue with the source tagged. This means the XReview dashboard or reviewer UI shows "cases with user pressure" and "cases detected by bots" separately, and presumably the evidence rollup logic also weights the two sources differently inferred. Confirming this requires the XReview server-side logic to be released. - The existence of the
bystander_impersonationtype means the impersonation response line is divided into finer categories. The supporting wording is: "bystander_impersonation (lane tags key on that type, not generic impersonation)". An impersonation case found by a bot is labeled as "impersonation reported by a third party" and routed to a separate lane. The reason it states explicitly not to use "generic impersonation" is that lane tag routing uses report_type itself as the discriminator. A wrong tag routes the report to the wrong team, SLA, and severity. - The XReview client scarecrow uses lives in the
useng.common.xreviewshared module, that is, thecom.twitter.useng.common.xreviewpackage. This means the intake is not scarecrow-only but is called in common by several X internal backends. For example, the grox PtOS pipeline could also put reports into XReview through this library inferred. These usages are likely to become clearer in the next commits inferred. - ⚠ Claim strengthened: the earlier canon understood X's handoff from automatic detection to human review as roughly "grox PtOS attaches a label and the policy service takes action." This commit confirms in code that one branch of that handoff shares the XReview queue with user reports through the same channel. In other words, XReview is not a queue that handles only user reports; it is also one of the destinations of automatic detection.
3Visibility rule policies move from heap assembly to static tables — X
Source xai-org/x-algorithm@85ac72a — public source code, 2026-09-02. In these notes' numbering, this is Gen4+14.
Background visibility-filtering (VF) is the service that decides, post by post, whether a post may be shown to this viewer. In Gen4+13 (2026-09-01, 7ba7768), the VF rule representation moved from struct-per-rule to a data-driven RuleSpec enum. However, RuleEngine (then named Policies) still held three heap-assembled Vec<Box<dyn Rule>> vectors. The rules themselves had become data literals, but the policies (rule sequences per safety level) remained runtime vecs. This commit (Gen4+14) is phase 2 of the restructuring and removes that last layer.
What we learnedA. The three policies became static tables in the program image instead of heap-assembled vectors
The rule trait and the heap-allocated rule vector are gone from the code verified —
visibility-filtering/rules/mod.rs.- What disappeared is the
Ruletrait andVec<Box<dyn Rule>>. - Three items were removed along with them, for −155 lines in mod.rs:
- the
Ruletrait definition (two methods, name and evaluate) - the
evaluate_rulesfunction, which iterated over theBox<dyn Rule>collection to build the verdict - 5 related unit tests
- the
- What disappeared is the
The three policies are now carried as static tables in the rodata section of the program image verified —
visibility-filtering/rules/registry.rs.- The table type is
&'static [&'static [RuleSpec]]. - All three policies (
FILTER_ALL_POLICY,TIMELINE_HOME_POLICY,TIMELINE_HOME_RECOMMENDATIONS_POLICY) arestatic. RuleEngine::new()no longer allocates on the heap and only wraps aNsfwGatingCountriesreference.
pub(super) struct Policy { rules: &'static [&'static [RuleSpec]], additional_rules: &'static [&'static [RuleSpec]], } static TIMELINE_HOME_SHARED_RULES: [&[RuleSpec]; 9] = [ author_rules::AUTHOR_STATE_DROPS, author_rules::SOCIALGRAPH_DROPS, tweet_rules::TWEET_LABEL_DROPS, tweet_rules::NULLCAST_DROP, tweet_rules::TES_HOME_DROPS, tweet_rules::SENSITIVE_VIEWER_DROPS, tweet_rules::EXCLUSIVE_TWEET_DROP, tweet_rules::NSFW_MEDIA_INTERSTITIALS, tweet_rules::NSFW_AUTHOR_INTERSTITIAL, ]; static TIMELINE_HOME_RECOMMENDATION_ONLY_RULES: [&[RuleSpec]; 5] = [ tweet_rules::RECS_MEDIA_DROPS, author_rules::OON_NSFW_AUTHOR_DROPS, tweet_rules::OON_TWEET_FLAG_DROPS, tweet_rules::OON_TWEET_LABEL_DROPS, author_rules::OON_USER_LABEL_DROPS, ]; static TIMELINE_HOME_POLICY: Policy = Policy::new(&TIMELINE_HOME_SHARED_RULES); static TIMELINE_HOME_RECOMMENDATIONS_POLICY: Policy = Policy { rules: &TIMELINE_HOME_SHARED_RULES, additional_rules: &TIMELINE_HOME_RECOMMENDATION_ONLY_RULES, };- The table type is
Policieswas renamed toRuleEngineverified.- The reason is that this struct no longer owns a rule vector. The old name no longer reflected its state (a collection).
- The new name makes explicit that it is a dispatch layer.
What we learnedB. The rule evaluation order and verdict approach are unchanged
A policy evaluates rules in order from the front verified —
visibility-filtering/rules/registry.rs.- The evaluation function is
Policy::evaluate, which iterates over the rule iterator in order. - The semantics are unchanged.
Dropshort-circuits immediately.- Even if
Interstitialcomes first,Dropwins if a later rule returnsDrop. - If
Allowholds until the end,decided_by = None.
pub(super) fn evaluate(&self, context: &RuleContext<'_>) -> Verdict { let mut action = VfAction::Allow; let mut decided_by = None; for rule in self.rules() { match rule.evaluate(context) { VfAction::Drop(reason) => { return Verdict { action: VfAction::Drop(reason), decided_by: Some(rule.name()), }; } VfAction::Interstitial(reason) if matches!(action, VfAction::Allow) => { action = VfAction::Interstitial(reason); decided_by = Some(rule.name()); } VfAction::Allow | VfAction::Interstitial(_) => {} } } Verdict { action, decided_by } }- The evaluation function is
What we learnedOther changes
- Added a new test that verifies the uniqueness of rule names across all 15 slices verified —
visibility-filtering/rules/tweet_rules.rs.wired_rule_names_are_unique_and_nonemptyguarantees that rule names in all 15 slices, 11 tweet-axis slices and 4 author-axis slices, are non-empty and have no duplicates.- This assertion matters because rule names are referenced as literals in audits, metrics and the golden corpus.
Canon update Briefly append the Gen4+14 VF refactor part after the Gen4+13 subsection of services/x.md. The Gen4+13 subsection already describes restructuring phase 1 in detail, so phase 2 records only what changed.
Why it matters Policies moved from the runtime heap to rodata, and the verdict semantics are unchanged.
- The whole rule graph can now be dispatched at program start without any allocation. The VF server can evaluate immediately after fork, and the rule list remains data that can be observed directly in the program image with
readelf. - The composition of 9 common home slices and 5 recommendation-only slices is the same as in Gen4+13, and the rule counts are also unchanged. However,
RuleEngine::rule_counts()now calls the staticPolicy::len()instead of counting vector lengths. - The
Policies→RuleEnginerename makes clear the direction of this refactor, which ran across four commits from Gen4+11 to Gen4+14.- Move rules from code to data.
- Move policies from the heap to rodata.
- Name things from the dispatch viewpoint.
- The next step is likely to load rule literals from a config file (TOML/JSON), which would allow changing rules without a code deployment inferred. However, that would give up the benefits of Rust static dispatch (inlining, jump tables), so whether it actually goes this way depends on later commits.
4Training checkpoints can be decrypted with a key from the file header, without a sidecar key file — X
Source xai-org/x-algorithm@85ac72a — public source code, 2026-09-02. It changed 5 files (+305/−88 in total). In these notes' numbering, this is Gen4+14.
Background The 2026-09-01 commit (Gen4+13) added an envelope-encrypting Orbax checkpoint writer to the xrex training code. It was the standard envelope pattern: a KMS-wrapped DEK (Data Encryption Key) is stored in a _DEK sidecar file, and that DEK symmetrically encrypts the checkpoint files. This commit (Gen4+14) lets the master key be extracted from the envelope header of the checkpoint file itself for decryption, even without the sidecar file. It also enforces required fields in encryption_context at save time and attaches a mint nonce automatically. That is, it tidies up the file format, key management and context schema one by one.
What we learnedA. Without a sidecar key file, the key is unwrapped from the file header to read the checkpoint
The function that reads the DEK uses the sidecar file if it exists and otherwise unwraps the key from the envelope header verified.
- The function is named
adopt_tree_dek. - The same function reads both checkpoints in the
_DEKsidecar format created in the 2026-09-01 commit (Gen4+13) and checkpoints in the future in-envelope derived format. - The migration path is backward-compatible.
def adopt_tree_dek(path: pathlib.Path, kms_client) -> bytes: ... if dek_path.exists(): entry = _read_wrapped_dek(dek_path) or {} raw = bytes(xai_kms.nfs.unwrap_shared_dek(kms_client, dek_path)) key_id = entry.get("key_id") else: raw = _unwrap_header_master(kms_client, path) key_id = "header" rank_logger.info("KMS unwrap OK for %s (key_id=%s)", path, key_id) return raw- The function is named
The envelope header of a checkpoint file is 4096 bytes and consists of the magic
XAIENC01and the wrapped_len/context_len fields verified —phoenix/python/training/xai-checkpointing/xai_checkpointing/dek.py.- The header structure is as follows.
- The first 8 bytes are the magic
XAIENC01. - At byte offset 28–32 is the length of the wrapped field (big-endian u32).
- At byte offset 32–36 is the context length (u32).
- The wrapped field and the context JSON follow within the 4096 bytes.
- The first 8 bytes are the magic
- If the wrapped field has the form
xai-dek1:<salt>:<master>, it is treated as the "derived" format; otherwise it is judged to be legacy per-file wrapping and an exception is raised.
_ENVELOPE_HEADER_SIZE = 4096 _ENVELOPE_FIXED_LEN = 36 _DERIVED_PREFIX = "xai-dek1:" def _header_wrapped_master(path: pathlib.Path) -> tuple[str, dict]: envelope = _first_envelope(path) head = envelope.read_bytes()[:_ENVELOPE_HEADER_SIZE] if len(head) < _ENVELOPE_FIXED_LEN or head[:8] != b"XAIENC01": raise ValueError(f"not an envelope: {envelope}") wrapped_len = int.from_bytes(head[28:32], "big") context_len = int.from_bytes(head[32:36], "big") start = _ENVELOPE_FIXED_LEN field = head[start : start + wrapped_len].decode() context = json.loads(head[start + wrapped_len : start + wrapped_len + context_len]) if not field.startswith(_DERIVED_PREFIX): raise ValueError( f"envelope at {envelope} carries a legacy per-file wrapped key, " "not the xai-dek1 derived form" ) _salt, master = field[len(_DERIVED_PREFIX) :].split(":", 1) ...- The header structure is as follows.
The read path changed to use the tensorstore driver
xai_encrypteddirectly verified —phoenix/python/training/xai-checkpointing/xai_checkpointing/load.py.- Previously (2026-09-01 commit, Gen4+13), the KMS-proxy KvStore (
xai_kms.KvServe) called KMS on every IO. Now the DEK is unwrapped once, and thexai_encrypteddriver performs the symmetric decryption directly. - This approach removes the KMS round-trip latency from the IO path. The DEK is unwrapped only once, and symmetric decryption is then done locally.
- Both the previous and the next file formats are supported.
raw = adopt_tree_dek(path, kms_client) kvstore_base = { "driver": "xai_encrypted", "base": {"driver": "file", "path": path.as_posix() + "/"}, "dek_b64": base64.b64encode(raw).decode(), } metadata_json = json.loads(_read_encrypted_file(path, "_METADATA", kvstore_base, ts_context)) return kvstore_base, metadata_json, _names_from_tree_metadata(metadata_json), ts_context- Previously (2026-09-01 commit, Gen4+13), the KMS-proxy KvStore (
What we learnedB. Encrypted saves require mandatory context keys and automatically insert a path hash as the nonce
At save time, encryption_context requires two keys verified —
phoenix/xrex/train/checkpoint_write.py.- The two keys are
domainandrun. - Judging by the names,
domainis the system domain this checkpoint belongs to (for example "recsys", "grox"), andrunis the training run id. - These two values are carried as the encryption_context of the KMS call, so the audit log records "which run of which domain unwrapped which checkpoint, and when".
- In other words, this code enforces the KMS audit flow.
missing_context = {"domain", "run"} - self.checkpoint_config.encryption_context.keys() if missing_context: raise ValueError( "encrypted checkpoint context is missing " + ", ".join(sorted(missing_context)) )- The two keys are
At save time,
mint_nonceis added to encryption_context automatically verified.- The value is the sha256 hash of the checkpoint path, inserted automatically as the
mint_noncevalue ofencryption_context. As a formula,mint_nonce = sha256(path). - This uses the standard AWS-KMS pattern in which KMS checks the context match at unwrap time, so an attempt to unwrap a different checkpoint with the same KMS key fails.
- For example, if an attacker copies a checkpoint file to a different path and tries to unwrap it, the mint_nonce does not match and KMS rejects the request.
encryption_context = dict(self.checkpoint_config.encryption_context) encryption_context["mint_nonce"] = hashlib.sha256(os.fsencode(path)).hexdigest() kms_client = xai_kms.KmsClient.from_cluster_env( key_id=self.checkpoint_config.encryption_key_id, encryption_context=encryption_context, )- The value is the sha256 hash of the checkpoint path, inserted automatically as the
What we learnedOther changes
The encryption option and the tensorstore save approach were separated verified —
phoenix/xrex/train/trainer.py._uses_tensorstore_saveno longer forces encryption.- Previously,
encrypt=Trueturned on the tensorstore save approach automatically. - Now
checkpoint_write.save_checkpointtakeskms_clientas an argument and assemblesorbax_encrypted.get_encrypted_checkpointerby itself.
def _uses_tensorstore_save(self) -> bool: return self.checkpoint_config.save_method == "tensorstore"The error message that prevents downgrading to plaintext saves became more specific verified.
- The previous message was "restored from an ENCRYPTED checkpoint but orbax writer cannot produce encrypted saves".
- The new message is "restored from an ENCRYPTED checkpoint but checkpoint_config.encrypt is false — refusing to downgrade to plaintext saves; set checkpoint_config.encrypt=true with encryption_key_id/encryption_context".
- The new message states explicitly why it failed and what to do.
_AsyncCheckpointerwas extended to wait for wait_until_finished of the orbax_encrypted module as well verified —phoenix/xrex/utils/checkpointing.py.- When the flush of a checkpoint save finishes, all encrypted IO is also guaranteed to be finished.
Canon update After the "envelope-encrypted Orbax checkpoint writer" block in the Gen4+13 subsection of services/x.md, append what changed in Gen4+14.
Why it matters The read and write paths for encrypted checkpoints are in place, but there is not yet evidence that they were turned on as a production default.
- This commit completes write-path verification (enforcing
domain/runand insertingmint_nonceautomatically) and two-format support on the read path. However, whetherCheckpointConfig.encrypt = Trueis on by default in the actual training config is not in this diff. This is still a checkpoint to watch. - The file format is now documented. The 4096-byte header, the
XAIENC01magic and the wrapped_len/context_len offsets remain in the code, so future external tools (checkpoint viewers, audit tools) can parse this format. - The
xai-dek1:prefix indicates a key derivation scheme inferred. The phrase "derived master" suggests a KDF-based approach in which a master key derived from a salt is placed in the header and the wrapping is placed in the wrapped field inferred. This contrasts with the legacy per-file wrapped key (a separately stored KMS-wrapped DEK for each file) inferred. The details of X.AI's own KDF spec ("xai-dek1") are not in the code. They are presumably in thexai_kms.nfsmodule inferred. - Removing the KMS-proxy KvStore matters for performance. Previously every IO (one ocdbt operation) required a KMS call, but now the DEK is unwrapped once and decrypted symmetrically in local code. KMS round trips drop from one per IO to one in total, so checkpoint restore latency is likely to fall as well inferred.
- The path that unwraps once is handled by
adopt_tree_dekverified —dek.py.
- The path that unwraps once is handled by
5The default two-tower training config uses QK-norm instead of a logit cap — X
Source xai-org/x-algorithm@85ac72a — public source code, 2026-09-02. In these notes' numbering, this is Gen4+14.
Background A two-tower is a retrieval model structure that embeds users and items with separate encoders (towers) and finds candidates by their similarity. In Gen4+6 (2026-08-21, 28e414f), three options, qk_norm=True, attn_logit_cap=-1 and right_anchored_rope=True, were first introduced in the nano config of the two-tower. However, the defaults of the composite base config at that time remained qk_norm=False and attn_logit_cap=80.0. This commit (Gen4+14) flips those defaults and makes QK-norm the actual default for composite two-tower training.
What we learnedA. The default config of the composite two-tower turns QK-norm on and the attention logit cap off
Two attention-related defaults changed in the base config of the composite two-tower verified —
phoenix/xrex/configs/xrecsys_two_tower.py.qk_norm: False → True: adds normalization (L2) to the attention query and key vectors. QK-norm is one of the standard techniques for stabilizing Transformer training.attn_logit_cap: 80.0 → -1: the cap (80) that clipped logit values before the attention softmax changed to -1 (= off). With QK-norm on, the QK dot product is already bounded, so a separate logit cap becomes unnecessary.
def _xrecsys_two_tower_combined_base() -> dict: return { ... "learning_rate": 2e-3, "emb_learning_rate": 0.1, "qk_norm": True, # was False "attn_logit_cap": -1, # was 80.0 "primer_norm": True, ... }
Canon update Briefly recorded this hyperparameter change in the Gen4+14 subsection of services/x.md.
Why it matters The training stabilization approach of two-tower retrieval moved from logit clamping to QK-norm. The two techniques are not the same, but they serve a similar purpose (controlling the dynamic range of soft attention). The 12 days from the option's introduction in Gen4+6 (2026-08-21, 28e414f) to the default switch in this Gen4+14 were likely needed because QK-norm showed an advantage in training loss or downstream metrics in a short A/B test inferred.
6Of 30 new papers, 0 passed the radar criteria — arXiv cs.IR
Of 30 new arXiv cs.IR papers (parsing succeeded, lookup normal), 0 passed the radar acceptance criteria. The criteria are platform-company authors or production deployment/A/B claims, novelty in feed/ranking mechanisms, and large-scale results. There was no new material from Meta or Netflix that day.
- ReST (From Language to Behavior: Scaling Sequence Transformers for Industrial Recommendation Ranking with Rec-Native Designs) fell in this triage window, but it was already added to radar.md new this month in yesterday's 2026-09-03 daily. It is not registered twice.
- hLLM is an interesting decoding optimization for generative reranking, but it did not pass the radar criteria and was rejected.
- The title is "Single Pass Decoding for Generative Reranking", and the authors are Emil Laftchiev, Prachi Agrawal, Moe Kayali and others.
- It is a decoding scheme that uses LoRA fine-tuning and the Hungarian algorithm to produce a ranking of N items in an O(1) forward pass, and it claims to reach 28ms end-to-end.
- It is a methods paper with no production deployment claim.
- The author affiliation would need to be checked through the open-review author profile, but the abstract alone has no platform-company signature.
- It is a candidate to review again if a production claim in this direction appears from a platform company.
- SPAR is a Generative POI Recommendation paper. It was rejected in principle because the research is limited to POI (radar domain restriction).
- GenCAR was also rejected. It is an academic experiment on OOD recommendation.
- All the rest were rejected as well. The reasons we confirmed fall into three groups.
- benchmark/RAG methodology (no platform production claim)
- domain restriction (POI, legal, medical, science)
- author affiliations mostly academic institutions
Canon update None (no new radar entries).
Corrections
None.
Radar
None.
What to check next
- Watch the downstream consumers of the backbone score — in this commit home-mixer only stores it on the candidate, and the code that consumes it is not yet open. Observing in later commits which of
MultiRiskAdsBlender,VMRanker, DPP re-rank and the side-effect logger consumes backbone_scores will reveal the actual purpose of the two-layer backbone/head architecture. - Watch which scarecrow bots call
CreateXReviewReportProd— the function unit is new, but the actual scarecrow rule definitions that call it (such as thebystander_impersonationdetection logic) are not yet in the repo. Once the rule DSL opens in later commits, it will show which bots file automatic reports under which conditions. - Watch for other users of the
useng.common.xreviewlibrary — check which of grox PtOS, home-mixer and phoenix files reports to XReview through this library. In particular, the key question is whether grox's adult content cross-validation results flow into the XReview lane. - Watch when
CheckpointConfig.encrypt=Trueappears in a production config — it was wired in Gen4+13 (2026-09-01), and this Gen4+14 put write-path verification and the read path in place. Next comes the switch of the config default. When it is turned on in the training config, the point at which X actually started encrypting training checkpoints at rest will be confirmed. - Watch the lane tag routing and evidence rollup logic of the XReview server — this commit exposed only the client-side schema. The
report_type→ lane tag mapping and the evidence rollup algorithm (N reports on the same content → verdict) can be confirmed only once the XReview server code is exposed in the repo. - Watch the
xai-dek1:KDF spec — the salt→master derivation algorithm for the derived master key carried in the envelope header. It will be confirmed when thexai_kms.nfsmodule appears in the public repo.