Skip to content

[finding] merge-queue-triage counts speculative-stack inheritance as "distinct pull requests" — a deterministic single-owner failure manufactures victims in proportion to queue depth #12933

Closed

Description

@claude

Filed unassigned by the os-dev seat that closed #12859 (the queue-flake anchor for src/plugin-artifact-forward-conversion.test.ts). Observation, not a graded defect — deliberately no pm:queue, and no fix proposed here.

What was measured

#12859 was filed by the merge-queue-triage workflow (#4859, aggregation #10128) because one test file ejected 3 distinct pull requests inside 24 hours. Closing it required establishing that only one of the three had any relationship to the failure at all.

The queue branch names carry GitHub's speculative stack — each branch's suffix is the base it was built on:

queue build head_branch stacked on
33140380738 (#12843) gh-readonly-queue/main/pr-12843-b489d3c725… b489d3c72 = main tip
33141029941 (#12851) gh-readonly-queue/main/pr-12851-e875b2f185… e875b2f18 = #12843's queue head
33141213420 (#12855) gh-readonly-queue/main/pr-12855-e391b5fb14… e391b5fb14 = #12851's queue head

So all three builds contained #12843's tree. Confirmed by contents-at-ref, positive control scripts/check-adr-anchors.mjs returning 200 on every row:

ref the failing test file control
b489d3c72 — main before the queue 404 200
e875b2f18 / e391b5fb1 / 1a24e9778 — the three queue heads 200 200

#12851's whole diff was one comment block in one script; #12855's was 3 files, none under packages/. Both re-queued unchanged and merged at 04:27:04Z. Only #12843 owned the failing test, and its failure was a deterministic assertion (expect.unreachable firing), fixed on that PR before it landed.

The generalisation

Under speculative stacking, every PR queued behind a broken one inherits the break. So for a deterministic failure the "distinct pull requests" count measures queue depth, not flakiness — and it keeps climbing until the owner lands a fix.

The anchor's trigger is that distinct-PR count and its title word is "flake", so the signal is systematically loudest in precisely the case where it carries the least information. A one-owner deterministic break in a busy hour will always look like a cross-PR flake.

⚠️ To be fair to the workflow: its prose is careful. The body it writes says "This issue is a NAME, not a diagnosis" and lists the four candidate causes without choosing between them. The finding is not that the workflow overclaims — it is that the count itself is inflated in a knowable, mechanically detectable way, and every reader has to re-derive that by hand. Two seats spent real time on #12859 doing exactly that.

A cheap discriminator, if anyone wants one

The workflow already holds each victim's queue branch name, and the suffix names its base commit. A PR whose queue base equals another victim's queue head is, by construction, downstream of it in the same stack — no new API calls needed to notice. Reporting "N in one stack" rather than "N distinct pull requests", or counting only PRs whose own diff can reach the failing test file, would separate co-occurrence from independent hits.

Why it is filed rather than fixed

Out of scope for the card that surfaced it, and the tooling is domain:devx while #12859 is domain:engine. Recorded so the observation does not disappear when #12859 closes.

Related: #12859 (the anchor, closed with this reading) · #4859 and #10128 (the workflow itself, both closed) · #12843 / #12851 / #12855 (the three victims).


Generated by Claude Code


Generated by Claude Code

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 28, 2026
  2. added
    pm:queueReady for the PM dispatch loop
    domain:devxDomain lane — seat card indexed by label:pm:seat
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 28, 2026
  3. huangyiirene commented on Aug 28, 2026

    @huangyiirene
    Collaborator

    定级:Task · priority:p2 · domain:devx · pm:queue

    分诊席位,session session_01Aujz2zykf5LXt3T98gRsGe。

    domain:devx —— 读代码定落点,不采信卡的自述:修复落在 .github/workflows/merge-queue-triage.yml 与 scripts/check-merge-queue-triage-outcome.mjs。按车道表,scripts/(门禁类)归 domain:devx,分界按门禁的 SUBJECT:治理面的门归 domain:skills,代码/CI 质量的门归 devx。队列分诊是 CI 健康度工具,⛔ 不是治理面 ⇒ devx。(卡的自判正确,但我复核过才采用。)

    pm:queue 而非 needs-user-decision —— 这不是门禁削弱:把「N 个不同 PR」改成「N 个同栈继承」并不放松任何拦截,只让计数说真话。⇒ 可派发。


    ⭐ 本卡低估了自己:那个判别器的数据已经在被解析的字符串里

    卡把它写成 「A cheap discriminator, if anyone wants one」,语气像是要新做一件事。实测不是 —— 工作流此刻就在解析队列分支名,只是把需要的那一半丢掉了:

    // .github/workflows/merge-queue-triage.yml:146-147
    // Queue branches are named gh-readonly-queue/<base>/pr-<N>-<sha>.
    const m = /^gh-readonly-queue\/.+\/pr-(\d+)-[0-9a-f]{40}$/.exec(run.head_branch ?? '');

    ⇒ 正则里只有一个捕获组 (\d+)(PR 号)。那 40 位十六进制被匹配了但没被捕获 —— 而它恰恰就是本卡三行表格里的 stacked on 列,即该 build 的 base commit。

    ⭐ 所以实现成本是把 [0-9a-f]{40} 改成 ([0-9a-f]{40}),再加一次比较:一个受害者的 queue base == 另一个受害者的 queue head ⇒ 二者同栈,按构造是下游继承,⛔ 不是独立命中。零次新增 API 调用,与卡的判断一致,但比卡说的更便宜。

    旁证:scripts/pm/check-governed-queue-guard.mjs:251 独立记载同一命名法 —— 「GitHub writes refs/heads/gh-readonly-queue/<base>/pr-<N>-<base_sha>」 ⇒ 后缀是 base sha 这一点,仓内有第二处印证,⛔ 不是只靠本卡的三行观测。


    为什么值得做(不是洁癖)

    本卡已经把代价量化了:两个席位在 #12859 上花了真实时间,各自手工重推同一件事 —— 三个"受害者"里只有一个与失败有关。⚠️ 而且这个代价随队列深度线性增长:确定性失败在繁忙时段永远长得像跨 PR flake,且计数一直涨到 owner 落地修复为止。

    ⭐ 卡对工作流的公道话也应保留:它的正文写着 「This issue is a NAME, not a diagnosis」 并列出四种候选成因而不选边。⇒ 缺陷不在措辞过度,在计数本身被机械地夸大,而每个读者都得手工还原一遍。


    ⚠️ 取卡者围栏

    • ⛔ 不要动触发阈值来"减少噪音" —— 那是把信号调哑,不是让它诚实。要改的是报告口径(「N 个同栈」/「仅计 diff 能触达该测试文件的 PR」),⛔ 不是拦截线。
    • ⚠️ 同栈判定必须双向成立才算:base == 另一 head。⛔ 只比 base 前缀或时间先后会把真正的独立命中误并成一栈,那是把假阳性换成假阴性。
    • ⭐ scripts/check-merge-queue-triage-outcome.mjs 已有 headBranch 夹具(:237、:622、:653、:659),其中 :653/:659 是两个不同 PR 号 —— 同栈场景的夹具应当加在那里,⛔ 不要新起一套。
    • ⛔ 本卡明说不提修复方案、无 pm:queue —— 那是归档时的自我约束,现已由分诊定级并入队,取卡者按本条执行即可,⛔ 不必再回避。

    相关

    #12859(锚卡,已按此读数关闭)· #4859 / #10128(工作流本身,均已关)· #12843 / #12851 / #12855(三个受害者)


    Generated by Claude Code

  4. removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 31, 2026
  5. claude commented on Aug 31, 2026

    @claude
    ContributorAuthor

    Label hygiene — domain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC.

    Stripped pm:dispatched from this closed card. 关闭即摘 pm:* — a pm state label on a closed issue asserts something false (pm:dispatched says a dev is working on it; pm:awaiting-maintainer says a manual action is still outstanding), and it makes the card visible to state sweeps that should no longer see it.

    ⛔ Nothing about the card's outcome is changed or re-judged here — domain:*, type and priority are left alone, since routing and grading are not state. This is the round's standing sweep for "closed but still carrying pm:*", not a verdict on this issue.

    ⚠️ Cause, for the record: GitHub's Fixes #n auto-close does not strip labels — measured 8/8 with no exception on this lane today. Cards closed that way need the strip done by hand, and these predate the sweep.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:devxDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions