ci(cmux-next): the session label comes only from a placement that judged its queue

Merged
TE
5d ago
+187-33

Leo, 2026-10-28: "Bro, I thought we already had logic to automatically route stuff. How can we still have pins like this? I hate pins. Like, just pin the set of the stuff that isn't hilariously slow." Rulebook: no job waits on one reserved label; "a CI test fails on a runs-on that can resolve to a single-host label with no fallback".

What was wrong

On 2026-10-29, #24295's daemon tests waited 90 minutes on glaeda-session-std-xcode-26.6, and then did it again. Five session runners were online and busy, and the gui label next to it was starting jobs within a minute. daemon-test, generated-files and both autofix action-exports jobs could still put that label into runs-on as a literal (vars.CI_SESSION_LABEL == '1' && 'glaeda-session-std-xcode-26.6' || ...), so no picker ever looked at its queue. GitHub never moves a queued job. ci-owned-pool-rescue.yml only watches cmux-next runs "refusals-only" when Blacksmith has no free slot, and that day it had none, so nothing moved those jobs.

What changed

  • Routes (cmux-next.yml daemon-test and generated-files runs-on plus CMUX_NEXT_RUNNER, cmux-next-pr-autofix.yml and cmux-next-base-autofix.yml action-exports): the session label is never written literally. When the placement gave no route, the last owned leg is glaeda-gui-std-xcode-26.6, and Blacksmith macOS comes after that, as before. The session label still reaches a job, but only through placement outputs (session_runner, free_session_runner, session_lane_runner).
  • Picker (side_lane_placement.py role_room(), next_macos_cascade.py Seen.unserved): the janitor's snapshot already writes unserved, the labels with a queued job and none running or started within 10 minutes (starved_labels.py). role_room() now refuses room on such a label unless one of its runners is idle right now. An empty session_runner then sends the jobs to gui_runner, and an empty gui route sends them to gui_overflow (Blacksmith). Since feat-cmux-next added two keeps to healthy_role_label() that skip role_room() (a stalled or dead Blacksmith overflow keeps any online role label, and a known not-full health reading keeps it), the same unserved-and-no-idle-runner check now runs first there too: an unserved queue is not a busy queue that moves. Seen.unserved is read only from a fresh snapshot.
  • The CI test that should have caught this (tests/test_ci_runs_on_fallback.py): its gui-or-session rule passed these jobs. They had a Blacksmith last leg, were gated on attempt, and sat in a workflow the rescue watches, so the test counted the rescue as their way off. The rescue doesn't move them when Blacksmith is full. The new test_a_session_label_comes_only_from_a_placement_that_judged_it fails when a runs-on names a session label literally, or reads a session variable without the placement's session_lane_runner verdict. Red on base with 8 findings (all four jobs, literal + vars.CI_SESSION_LABEL); green here. A self-test covers the expression forms.
  • Tests updated to match: test_ci_session_label_routing.py, test_ci_side_lane_placement.py (autofix route), test_cmux_next_checks_workflow.py (generated-files expected route), test_ci_self_hosted_guard.sh (drops the session-selector exemptions that no longer match anything), test_ci_next_macos_cascade.py (unserved parsing: single owned keys only).

Other single-label literals checked

  • glaeda-gui-std-xcode-26.6 (cmux-next.yml, autofix): it comes after placement and before a Blacksmith leg, and the rescue watches these workflows. role_label_problems already covers it.
  • glaeda-dev-std-xcode-26.6 (FRESH_LABEL env): placement input only, never runs-on.
  • glaeda-side-light-xcode-26.6 (cmux-tui-nightly.yml macos_runner): a pool label, and its re-run takes macos_retry_runner (Blacksmith).
  • glaeda-root-std-xcode-26.6 (ci-compile-attribution.yml OWNED): a probe input with its own OVERFLOW.

None of these is a session label left without a judge, so they stay as they are.

Reds

  • tests/test_ci_fork_runner_routing.py (SESSION_LANE: ${{ vars.CI_SESSION_LANE_RUNNER }} at cmux-next.yml placement) fails the same way on base. #27548 fixes it.
  • check_no_self_hosted_fleet_runners in test_ci_self_hosted_guard.sh flags the same five runs-on lines on base.

No new timer, loop or service. Nightly, dev and promote workflows are untouched, and no concurrency or cancel behaviour changed.

Agent session: agent session

šŸ¤– Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes cmux-next jobs waiting hours on session labels whose runners are online but busy, by ensuring the session label only reaches a job through a placement that judged its queue.

  • daemon-test, generated-files, and both autofix action-exports jobs no longer name the session label literally; their last owned leg is the gui label.
  • role_room() in side_lane_placement.py declines labels the janitor finds unserved (a job queued, none running or started within 10 minutes) unless a runner is idle, so session_runner falls back to gui_runner; healthy_role_label() applies the same refusal before the stalled-overflow and health-contract keeps bypass it.
  • The new test_ci_runs_on_fallback.py test fails whenever a runs-on names a session label literally or reads a session variable without the placement's session_lane_runner verdict; it fails on base with 8 findings and is green here.

Migration

  • No new timers, loops, or services; nightly, dev, and promote workflows are untouched.