From 4a00f9e5cd5cc7080b490f2667518eaf158366c4 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 19 Sep 2026 15:01:51 +0000 Subject: [PATCH 1/4] cores_per_slot = 1 per operator ruling; the CPU axis stops binding, CPUQuota stays unset Operator ruling, 2026-09-19, verbatim: "there is no CPU demand - the processes we run are single core - do NOT overthink it please". gunbc.runner_slot_allocation: cores_per_slot is an authored POLICY of 1 (gunbc_runner_cores_per_slot_policy), not derived from memory_max and not from a vendor price list. The memory-per-core ratio path is deleted with its refusal vocabulary (RunnerMemoryPerCoreResolution incl. the unreachable MemoryPerCoreUnresolved arm, RunnerCoresPerSlotResolution, the survey imports). The CPU axis admits 128 and stops binding; committed widths become the memory admissions: srv1 15, srv3/srv4 17. host_cpu_admitted_width and the derived width lose their unreachable unresolved arms. cpu_quota was derived from cores_per_slot and would have rendered CPUQuota=100% -- a throttle on parallel link/codegen the ruling did not ask for. RunnerSlotCpuQuota's refusing arm (no producer left) becomes SlotCpuQuotaUnbounded, the production value. The unset is affirmative so a previously installed finite quota is CLEARED, not merely no longer emitted: extdeps.systemd.unit_file gains DirectiveReset (the empty assignment that systemd.resource-control(5) documents as CPUQuota's unset) and slice_cpu_quota_unbounded(); host_converge renders the per-slot drop-in knob as `CPUQuota=` under RunnerCpuBoundary::NoCpuCeiling; fabric_cell_effect always emits six directives with `CPUQuota=` as the sixth; the fabric cell desired digest spells unbounded through extdeps.systemd's systemd_cpu_quota_unbounded_identity so a host printing `infinity` reconciles as satisfied. ci_runner_placement's boundary now follows the slot's cpu_quota rather than re-deriving from cores_per_slot. cpu_weight=100 unchanged. Witnesses migrated to the policy: the vendor-ratio rows are deleted with their subject; committed widths pin 15/17/17 == memory admission with CPU at 128; both quota arms are driven by fixture; the sudoers witness asserts `CPUQuota=` and no `%` grant. Sudoers projections regenerated through generated_artifact_gate main_wet. Pre-existing reds not introduced here: five rows of test.claim.host_allocation_conservation pin 16 GiB-era widths and FAIL identically on a clean main ancestor (72619645109); outside the required gate. Supersedes #11684. Co-Authored-By: Claude Opus 5 (1M context) --- dag/extdeps/systemd/systemd.dag | 7 +- dag/extdeps/systemd/unit_file.dag | 17 + dag/gunbc/ci/ci_runner_placement.dag | 89 +++-- dag/gunbc/fabric/fabric_cell_converge.dag | 91 ++--- dag/gunbc/fabric/fabric_cell_effect.dag | 80 ++-- dag/gunbc/fleet/fleet_converge_plan.dag | 3 - ...er_microvm_guest_size_derivation_stall.dag | 7 +- dag/gunbc/host/host_converge.dag | 47 ++- dag/gunbc/runner/runner_deploy_emit.dag | 7 +- dag/gunbc/runner/runner_host_grants.dag | 21 +- dag/gunbc/runner/runner_microvm.dag | 31 +- dag/gunbc/runner/runner_slot_allocation.dag | 359 +++++------------- ...utor_privileged_operation_witness_test.dag | 92 ++--- .../fabric_cell_converge_witness_test.dag | 45 +-- .../fabric_cell_effect_witness_test.dag | 51 ++- .../host_allocation_conservation_test.dag | 77 +--- .../runner/runner_microvm_witness_test.dag | 53 +-- .../runner/runner_placement_witness_test.dag | 6 +- .../runner_slot_allocation_witness_test.dag | 67 ++-- .../claim/systemd_unit_file_witness_test.dag | 14 +- provisioning/srv1/gunbc-ghrunner.sudoers | 9 + provisioning/srv3/gunbc-ghrunner.sudoers | 17 +- provisioning/srv4/gunbc-ghrunner.sudoers | 15 + 23 files changed, 524 insertions(+), 681 deletions(-) diff --git a/dag/extdeps/systemd/systemd.dag b/dag/extdeps/systemd/systemd.dag index d7e5598a064..5862e2ec269 100644 --- a/dag/extdeps/systemd/systemd.dag +++ b/dag/extdeps/systemd/systemd.dag @@ -274,9 +274,14 @@ fn parse_systemd_cpu_quota_per_sec_show(raw: String) -> SystemdCpuQuotaPerSec { // COMPARABLE ARMS HAVE AN IDENTITY STRING; UNDECODABLE HAS NONE. Desired never produces Undecodable: // it renders through systemd_cpu_quota_per_sec_magnitude_identity of of_threads, which is total. +// THE UNBOUNDED IDENTITY IS A NAMED CONSTANT because a DESIRED side that declares no ceiling has to +// spell it the same way the observed side does, and reaching that spelling through the identity +// function would hand the desired side an Optional it can never see Absent on. +data systemd_cpu_quota_unbounded_identity: String = "infinity" + fn systemd_cpu_quota_per_sec_identity(q: SystemdCpuQuotaPerSec) -> String? { match q { - CpuQuotaUnbounded => Present { value: "infinity" } + CpuQuotaUnbounded => Present { value: systemd_cpu_quota_unbounded_identity } CpuQuotaPerSec { usec: u } => Present { value: systemd_cpu_quota_per_sec_magnitude_identity(usec: u) } CpuQuotaAbsent => Present { value: "" } CpuQuotaUndecodable { raw: _ } => none diff --git a/dag/extdeps/systemd/unit_file.dag b/dag/extdeps/systemd/unit_file.dag index 54e15ca194b..535e5a7df19 100644 --- a/dag/extdeps/systemd/unit_file.dag +++ b/dag/extdeps/systemd/unit_file.dag @@ -449,6 +449,7 @@ type SystemdDirectiveValue | DirectiveWeight { weight: Int } | DirectiveCpuTime { threads: HardwareThreadCount } | DirectiveUnlimited + | DirectiveReset fn systemd_directive_value_wire(value: SystemdDirectiveValue) -> String { match value { @@ -457,9 +458,18 @@ fn systemd_directive_value_wire(value: SystemdDirectiveValue) -> String { DirectiveWeight { weight: w } => to_string(w) DirectiveCpuTime { threads: t } => join([to_string(hardware_thread_count_value(t: t) * 100), "%"], "") DirectiveUnlimited => systemd_directive_unlimited_wire + DirectiveReset => systemd_directive_reset_wire } } +// AN EMPTY ASSIGNMENT RESETS THE SETTING TO ITS DEFAULT. systemd.syntax(7) states it for the unit +// file, and systemd.resource-control(5) restates it for the one knob that has no `infinity` +// spelling: "Setting CPUQuota= to an empty value unsets the quota" -- `CPUQuota=infinity` is not a +// value that directive's parser admits, so `DirectiveUnlimited` cannot serve it. `systemctl +// set-property` reads the same empty assignment as unset. `DirectiveReset` is the write-side arm +// for that wire; the read side already carries the state it produces as `CpuQuotaUnbounded`. +data systemd_directive_reset_wire: String = "" + // systemd's sentinel for "no limit", spelled once. `max` is the cgroup v2 kernel interface's word; // `infinity` is systemd's, and a unit file is read by systemd rather than by the kernel. data systemd_directive_unlimited_wire: String = "infinity" @@ -521,6 +531,13 @@ fn slice_cpu_quota(threads: HardwareThreadCount) -> SystemdSliceDirective { SystemdSliceDirective { property: CPUQuota, value: DirectiveCpuTime { threads: threads } } } +// NO CEILING ON THE CPU AXIS IS AN ASSIGNMENT, NOT AN OMISSION. A slice that was once given a +// quota keeps it until something unsets it, so a desired state of "unbounded" that emitted nothing +// would converge only on cells that never had one. This mint is the affirmative unset. +fn slice_cpu_quota_unbounded() -> SystemdSliceDirective { + SystemdSliceDirective { property: CPUQuota, value: DirectiveReset } +} + // The directive names are systemd's, spelled once here. A consumer never writes the literal // "ExecStart"; it constructs `ExecStart { command }` and this module decides how that renders. diff --git a/dag/gunbc/ci/ci_runner_placement.dag b/dag/gunbc/ci/ci_runner_placement.dag index 5971db60edb..7a9c38308ff 100644 --- a/dag/gunbc/ci/ci_runner_placement.dag +++ b/dag/gunbc/ci/ci_runner_placement.dag @@ -11,11 +11,11 @@ import std.disposition { Disposition, Scaffold, SingleAuthority } import std.decl_ref { DeclarationRef, WholeDeclaration } import gunbc.runner_spec_from_offer { runner_spec_from_offer } import gunbc.runner_slot_allocation { - CoresPerSlotResolved, - CoresPerSlotUnresolved, + RunnerSlotCpuQuota, + CpuQuotaResolved, + SlotCpuQuotaUnbounded, gunbc_runner_slot_desired, gunbc_runner_slots_per_host, - gunbc_runner_cores_per_slot, } import gunbc.session_reservation_policy { SessionReservationCharged, SessionReservationUnsized, @@ -109,17 +109,24 @@ fn workload_class_cpu_weight(wc: WorkloadClass) -> Int { // amount of anything. CPUQuota= is an ABSOLUTE ceiling on CPU time per wall second. AllowedCPUs= // is a third thing again -- a pinning of the unit to named CPUs. // -// CapacityQuota is the no-overcommit contract this fleet adopts: each slot may consume at most -// cores_per_slot threads' worth of CPU time, and the sum of the runner, compile-pool and host -// reserve ceilings must fit the machine. Pinning each slot to a pairwise-disjoint SET of named CPUs -// is a stronger contract again, and this fleet does not make it; the block below records why it has -// no constructor rather than a variant. +// CapacityQuota is a no-overcommit contract: each slot may consume at most that many threads' +// worth of CPU time, and the sum of the runner, compile-pool and host reserve ceilings must fit +// the machine. NoCpuCeiling is the contract this fleet ADOPTS as of the 2026-09-19 operator ruling +// ("there is no CPU demand - the processes we run are single core"): a slot carries CPUWeight only +// and no CPUQuota, which is the state the hosts run in today, and a 100% quota at the policy of +// one core per slot would throttle the parallel link/codegen phases of a job that is otherwise +// single-core. The arm is read from gunbc.runner_slot_allocation's cpu_quota, never derived from +// cores_per_slot here: width admission and the per-slot ceiling are two facts of one row, and +// deriving one from the other is what bound the fleet at a tenfold over-reservation. +// Pinning each slot to a pairwise-disjoint SET of named CPUs is a stronger contract again, and +// this fleet does not make it; the block below records why it has no constructor rather than a +// variant. // -// THE ARM THIS TYPE REPLACES WAS NEITHER OF THEM. cores_per_slot was consumed to decide how many -// units to create and then dropped, so the width was CPU-derived while the units it created carried -// no CPU directive at all -- srv1 and srv2 show MemoryMax as their only resource line, CPUWeight -// unset and CPUQuota infinity. Admission without confinement: the arithmetic said six threads per -// slot and the host was never told. +// NoCpuCeiling IS NOT THE "ADMISSION WITHOUT CONFINEMENT" ARM THIS TYPE ONCE REPLACED. That arm +// derived a width from cores_per_slot and then told the host nothing, so the units ran unconfined +// while the model believed them confined. NoCpuCeiling realizes as an affirmative `CPUQuota=` +// reset on every unit (gunbc.host_converge runner_cpu_boundary_knobs), so the model and the host +// agree that there is no ceiling; a unit that once carried one is converged back to none. // THE PINNING CONTRACT HAS NO CONSTRUCTOR, AND THAT IS THE POINT RATHER THAN AN OMISSION. // // An earlier revision carried a DisjointCpuSet arm so the promise this fleet does NOT make would have @@ -144,6 +151,7 @@ fn workload_class_cpu_weight(wc: WorkloadClass) -> Int { // this deletion removes. type RunnerCpuBoundary = CapacityQuota { threads: HardwareThreadCount } + | NoCpuCeiling type RunnerCpuBoundaryResolution = RunnerCpuBoundaryDerived { boundary: RunnerCpuBoundary } @@ -167,9 +175,15 @@ type RunnerCpuBoundaryResolution // // NEXT-RUNG TRIGGER: a compile-pool CPU charge in the host allocation, at which point this arm // derives a rebalanced width and ceiling instead of refusing. -fn runner_cpu_boundary_for(placement: CompilePoolPlacement, per_slot: HardwareThreadCount) -> RunnerCpuBoundaryResolution { +fn runner_cpu_boundary_for(placement: CompilePoolPlacement, quota: RunnerSlotCpuQuota) -> RunnerCpuBoundaryResolution { match placement { - CompilePoolInRunnerSlots => RunnerCpuBoundaryDerived { boundary: CapacityQuota { threads: per_slot } } + CompilePoolInRunnerSlots => + RunnerCpuBoundaryDerived { + boundary: match quota { + CpuQuotaResolved { threads: t } => CapacityQuota { threads: t } + SlotCpuQuotaUnbounded => NoCpuCeiling + } + } CompilePoolSliceDeclared { slice_unit: u } => RunnerCpuBoundaryUnchargeable { reason: concat("compile pool is a declared host slice and carries no modeled CPU charge, so a per-slot CPU ceiling derived from cores_per_slot alone would commit the host past its threads once that pool runs: rederive runner width and the per-slot ceiling together with the pool's CPU charge. Slice: ", u) @@ -215,7 +229,6 @@ type HostRunnerDeploymentRefusal | ZeroRunnerWidth | JobserverCitationAbsent { cause: String } | HostCpuAllocationUnchargeable { cause: String } - | RunnerCoreRatioUnresolved { cause: String } | HostNotEnrolledInFleet type RefusedHostDeployment { @@ -511,7 +524,6 @@ fn host_deployment_refusal_text(c: HostRunnerDeploymentRefusal) -> String { ZeroRunnerWidth => "zero_runner_width" JobserverCitationAbsent { cause: t } => concat("jobserver_citation_absent ", t) HostCpuAllocationUnchargeable { cause: t } => concat("host_cpu_allocation_unchargeable ", t) - RunnerCoreRatioUnresolved { cause: t } => concat("runner_core_ratio_unresolved ", t) HostNotEnrolledInFleet => "host_not_enrolled_in_fleet" } } @@ -602,30 +614,25 @@ fn host_standing_with_session_slice( cause: concat("no cited jobserver configuration, so the compile-token budget has no derivation and the host cannot be told a bound it did not compute: ", host_id) }) JobserverCited { config: jcfg, provenance: _ } => - match gunbc_runner_cores_per_slot() { - CoresPerSlotUnresolved { cause: rc } => - refuse_host(host: host_id, cause: RunnerCoreRatioUnresolved { cause: rc }) - CoresPerSlotResolved { threads: per_slot_threads } => - match runner_cpu_boundary_for( - placement: gunbc_compile_pool_placement, - per_slot: per_slot_threads - ) { - RunnerCpuBoundaryUnchargeable { reason: cr } => - refuse_host(host: host_id, cause: HostCpuAllocationUnchargeable { cause: cr }) - RunnerCpuBoundaryDerived { boundary: cpub } => - admit_or_refuse_zero_width( - dep: conservation_host_deployment( - offer: offer, - build_tokens: derive_jobserver_tokens( - nproc: cpu_facts_threads(cpu: cpu), - mem_total: host_ram, - cfg: jcfg - ), - cpu_boundary: cpub - ), - host_id: host_id - ) - } + match runner_cpu_boundary_for( + placement: gunbc_compile_pool_placement, + quota: gunbc_runner_slot_desired().cpu_quota + ) { + RunnerCpuBoundaryUnchargeable { reason: cr } => + refuse_host(host: host_id, cause: HostCpuAllocationUnchargeable { cause: cr }) + RunnerCpuBoundaryDerived { boundary: cpub } => + admit_or_refuse_zero_width( + dep: conservation_host_deployment( + offer: offer, + build_tokens: derive_jobserver_tokens( + nproc: cpu_facts_threads(cpu: cpu), + mem_total: host_ram, + cfg: jcfg + ), + cpu_boundary: cpub + ), + host_id: host_id + ) } } } diff --git a/dag/gunbc/fabric/fabric_cell_converge.dag b/dag/gunbc/fabric/fabric_cell_converge.dag index c9ec15441db..28c03b954ed 100644 --- a/dag/gunbc/fabric/fabric_cell_converge.dag +++ b/dag/gunbc/fabric/fabric_cell_converge.dag @@ -9,6 +9,7 @@ import std.measure { byte_size_count } import extdeps.systemd { systemd_cpu_quota_per_sec_of_threads, systemd_cpu_quota_per_sec_magnitude_identity, + systemd_cpu_quota_unbounded_identity, } import product.placement_supply { HostIdentity } import product.fabric.sanitation { CellId } @@ -18,7 +19,7 @@ import gunbc.build_cache_instance { RunnerSlotIdentity, runner_slot_instance_nam import gunbc.runner_slot_allocation { fabric_execution_slot_identities, gunbc_runner_slot_desired, - RunnerSlotCpuQuota, CpuQuotaResolved, CpuQuotaRefused, + RunnerSlotCpuQuota, CpuQuotaResolved, SlotCpuQuotaUnbounded, runner_slot_membership, CommittedSlotMember, SlotOutsideCommittedPopulation, @@ -204,31 +205,33 @@ fn fabric_cell_member_ownership(member: FabricCellMember) -> Optional // address instead of drifting silently. The digest is over the values the boundary will carry. fn fabric_cell_resource_boundary_state_digest() -> NonEmptyStr { let desired = gunbc_runner_slot_desired() - match desired.cpu_quota { - CpuQuotaRefused { cause: c } => - join(["cpu_quota_survey_refused=", c], "") as NonEmptyStr + fabric_cell_boundary_state_digest_of( + memory_max: to_string(byte_size_count(b: desired.memory_max)), + memory_high: to_string(byte_size_count(b: desired.memory_high)), + memory_swap_max: to_string(byte_size_count(b: desired.memory_swap_max)), + tasks_max: to_string(desired.tasks_max), + cpu_weight: to_string(desired.cpu_weight), + cpu_quota_per_sec: fabric_cell_desired_cpu_quota_identity(quota: desired.cpu_quota), + ) +} + +// THE QUOTA MEMBER IS THE IDENTITY THE OBSERVED SIDE PRODUCES, NOT A SECOND RENDERER AND NOT A +// SENTINEL. A resolved quota is the magnitude identity of systemd_cpu_quota_per_sec_of_threads: +// desired authors a thread count; observed reads CPUQuotaPerSecUSec as a formatted span, parses it, +// and supplies the same identity, so a six-thread cell and a host that printed `6s` compare equal +// as 6000000 microseconds. An UNBOUNDED quota is spelled by extdeps.systemd's +// systemd_cpu_quota_unbounded_identity -- the same constant the read-side identity returns for the +// CpuQuotaUnbounded arm a host that printed `infinity` parses to -- so a cell with no ceiling and a +// desired row with no ceiling compare equal through one spelling rather than a literal authored +// here beside the parser's. +fn fabric_cell_desired_cpu_quota_identity(quota: RunnerSlotCpuQuota) -> String { + match quota { CpuQuotaResolved { threads: t } => - fabric_cell_boundary_state_digest_of( - memory_max: to_string(byte_size_count(b: desired.memory_max)), - memory_high: to_string(byte_size_count(b: desired.memory_high)), - memory_swap_max: to_string(byte_size_count(b: desired.memory_swap_max)), - tasks_max: to_string(desired.tasks_max), - cpu_weight: to_string(desired.cpu_weight), - cpu_quota_per_sec: systemd_cpu_quota_per_sec_magnitude_identity( - usec: systemd_cpu_quota_per_sec_of_threads(threads: t), - ), - ) + systemd_cpu_quota_per_sec_magnitude_identity(usec: systemd_cpu_quota_per_sec_of_threads(threads: t)) + SlotCpuQuotaUnbounded => systemd_cpu_quota_unbounded_identity } } -// THE QUOTA MEMBER IS THE MAGNITUDE IDENTITY OF systemd_cpu_quota_per_sec_of_threads, NOT A SECOND -// RENDERER AND NOT A SENTINEL. Desired authors a thread count; observed reads CPUQuotaPerSecUSec as -// a formatted span, parses it, and supplies the same identity. A six-thread cell and a host that -// printed `6s` compare equal as 6000000 microseconds. A refused survey has no magnitude and is not -// stuffed into this field: fabric_cell_resource_boundary_state_digest names the refusal on its own -// arm, and fabric_cell_address_standing_for_quota blocks every disposition so reconcile cannot -// treat that string as drift-to-repair. - // THE SPELLING IS A FUNCTION, BECAUSE THE OBSERVED SIDE HAS TO PRODUCE THE SAME ONE. The desired // digest above is compared against a digest an OBSERVER builds out of what a host reported, and if // each side spells the record itself then the two spellings are a fork: a reordered key or a @@ -684,46 +687,24 @@ fn fabric_cell_address_disposition_ref(d: FabricCellAddressDisposition) -> Fabri } } -fn fabric_cell_address_standing_for_quota( +fn fabric_cell_address_standing( slot: RunnerSlotIdentity, d: FabricCellAddressDisposition, - quota: RunnerSlotCpuQuota, ) -> FabricCellAddressStanding { - match quota { - CpuQuotaRefused { cause: c } => - StandingBlocked { - disposition: AddressUnreadable { - ref: fabric_cell_address_disposition_ref(d: d), - cause: join(["desired CPU quota refused: ", c], ""), - }, - } - CpuQuotaResolved { threads: _ } => - match d { - AddressUnreadable { ref: _, cause: _ } => StandingBlocked { disposition: d } - AddressContradicted { ref: _ } => StandingBlocked { disposition: d } - AddressMissingOnHost { ref: r } => StandingExplicitlyAbsent { ref: r } - AddressUnobserved { ref: _ } => StandingBlocked { disposition: d } - AddressPresent { ref: r, member: m } => - if (m.state_digest as String) == (fabric_cell_desired_state_digest(slot: r.slot, address: r.address) as String) { - StandingSatisfied { ref: r } - } else { - StandingObservedDrift { ref: r, observed_state_digest: m.state_digest } - } + match d { + AddressUnreadable { ref: _, cause: _ } => StandingBlocked { disposition: d } + AddressContradicted { ref: _ } => StandingBlocked { disposition: d } + AddressMissingOnHost { ref: r } => StandingExplicitlyAbsent { ref: r } + AddressUnobserved { ref: _ } => StandingBlocked { disposition: d } + AddressPresent { ref: r, member: m } => + if (m.state_digest as String) == (fabric_cell_desired_state_digest(slot: r.slot, address: r.address) as String) { + StandingSatisfied { ref: r } + } else { + StandingObservedDrift { ref: r, observed_state_digest: m.state_digest } } } } -fn fabric_cell_address_standing( - slot: RunnerSlotIdentity, - d: FabricCellAddressDisposition, -) -> FabricCellAddressStanding { - fabric_cell_address_standing_for_quota( - slot: slot, - d: d, - quota: gunbc_runner_slot_desired().cpu_quota, - ) -} - fn fabric_cell_address_standings( readback: FabricCellReadback, slot: RunnerSlotIdentity, diff --git a/dag/gunbc/fabric/fabric_cell_effect.dag b/dag/gunbc/fabric/fabric_cell_effect.dag index c94be4e30df..950eb9de0d6 100644 --- a/dag/gunbc/fabric/fabric_cell_effect.dag +++ b/dag/gunbc/fabric/fabric_cell_effect.dag @@ -10,13 +10,14 @@ import extdeps.systemd.unit_file { slice_tasks_max, slice_cpu_weight, slice_cpu_quota, + slice_cpu_quota_unbounded, } import gunbc.build_cache_instance { RunnerSlotIdentity } import gunbc.runner_slot_allocation { gunbc_runner_slot_desired, RunnerSlotCpuQuota, CpuQuotaResolved, - CpuQuotaRefused, + SlotCpuQuotaUnbounded, } import gunbc.membership_reconcile { MemberAction, @@ -97,58 +98,46 @@ fn fabric_cell_base_directory_effect() -> FabricCellEffect { // cannot occur -- permanently red by staleness, the mirror of the permanently-green decoration // DESIGN 4b forbids. // -// FIVE BASE DIRECTIVES PLUS THE QUOTA, OR A TYPED REFUSAL. Every magnitude comes from +// FIVE BASE DIRECTIVES PLUS THE CPU QUOTA LINE, ALWAYS SIX. Every magnitude comes from // gunbc_runner_slot_desired -- the same row fabric_cell_resource_boundary_state_digest renders -// into the desired member digest, quota included via systemd_cpu_quota_per_sec_of_threads -- so the -// directives this effect applies and the digest reconcile compares are two projections of one -// authority and cannot disagree by drift. +// into the desired member digest -- so the directives this effect applies and the digest reconcile +// compares are two projections of one authority and cannot disagree by drift. // -// A REFUSED SURVEY DOES NOT RENDER FIVE DIRECTIVES. That list is a slice with memory and weight -// ceilings and no CPUQuota -- unbounded CPU that looks provisioned, the widen DESIGN §5 forbids -// and this module already named at the all-or-nothing fold. Substituting a fabricated percentage -// is the other fail-open. The honest arm is SliceDirectivesQuotaSurveyRefused, consumed by -// realization as CellCpuQuotaSurveyRefused so an ADD of an absent cell cannot apply the slice. +// THE QUOTA LINE IS EITHER A CEILING OR AN AFFIRMATIVE UNSET, NEVER ABSENT. CpuQuotaResolved +// renders `CPUQuota=%`; SlotCpuQuotaUnbounded renders `CPUQuota=` through +// slice_cpu_quota_unbounded, which is the empty assignment systemd reads as "no quota". Omitting +// the line under Unbounded would be the widen DESIGN 5 forbids in the one case that matters: a +// cell that once carried a quota would keep it while the desired digest said infinity, and the +// reconcile would report MemberChanged forever. The RunnerSlotCpuQuota that used to carry a +// refusing arm no longer does (its only refusing producer, a vendor survey, is deleted), so this +// fold is total and the SliceDirectivesQuotaSurveyRefused / CellCpuQuotaSurveyRefused arms that +// carried that refusal are deleted with it rather than left as arms nothing can produce. // // WHY DIRECTIVES AND NOT A SystemdUnitFile: the effect's production realization is // `systemctl set-property =...` -- systemd renders and owns the persisted // drop-in itself, so the privileged boundary is crossed by exactly these typed words and by no // authored file bytes (see the closed-sum note above). A unit-file carrier here would be a // representation of an artifact nothing constructs. -type FabricCellSliceDesiredDirectives - = SliceDirectivesComplete { directives: List } - | SliceDirectivesQuotaSurveyRefused { cause: String } - -fn fabric_cell_slice_desired_directives_for(quota: RunnerSlotCpuQuota) -> FabricCellSliceDesiredDirectives { +fn fabric_cell_slice_desired_directives_for(quota: RunnerSlotCpuQuota) -> List { let desired = gunbc_runner_slot_desired() - let base = [ + [ slice_memory_max(bytes: desired.memory_max), slice_memory_high(bytes: desired.memory_high), slice_memory_swap_max(bytes: desired.memory_swap_max), slice_tasks_max(count: desired.tasks_max), slice_cpu_weight(weight: desired.cpu_weight), + fabric_cell_slice_quota_directive(quota: quota), ] - match fabric_cell_slice_quota_directives(quota: quota) { - SliceCpuQuotaDirective { directive: d } => - SliceDirectivesComplete { directives: concat(base, [d]) } - SliceCpuQuotaSurveyRefused { cause: c } => - SliceDirectivesQuotaSurveyRefused { cause: c } - } } -fn fabric_cell_slice_desired_directives() -> FabricCellSliceDesiredDirectives { +fn fabric_cell_slice_desired_directives() -> List { fabric_cell_slice_desired_directives_for(quota: gunbc_runner_slot_desired().cpu_quota) } -type CellSliceQuotaDirective - = SliceCpuQuotaDirective { directive: SystemdSliceDirective } - | SliceCpuQuotaSurveyRefused { cause: String } - -fn fabric_cell_slice_quota_directives(quota: RunnerSlotCpuQuota) -> CellSliceQuotaDirective { +fn fabric_cell_slice_quota_directive(quota: RunnerSlotCpuQuota) -> SystemdSliceDirective { match quota { - CpuQuotaResolved { threads: t } => - SliceCpuQuotaDirective { directive: slice_cpu_quota(threads: t) } - CpuQuotaRefused { cause: c } => - SliceCpuQuotaSurveyRefused { cause: c } + CpuQuotaResolved { threads: t } => slice_cpu_quota(threads: t) + SlotCpuQuotaUnbounded => slice_cpu_quota_unbounded() } } @@ -163,7 +152,6 @@ type FabricCellRealizationRefusal | CellRosterDuplicateKey { address_wire: String } | CellChangeNotYetRealizable { address_wire: String } | CellTeardownNotYetRealizable { address_wire: String } - | CellCpuQuotaSurveyRefused { address_wire: String, cause: String } type FabricCellAddressRealization = AddressRealizable { effect: FabricCellEffect } @@ -173,10 +161,8 @@ type FabricCellAddressRealization // decides WHETHER there is one. Neither axis is collapsed into the other, so a new address and a // new action each fail to compile here rather than inheriting an arm that happened to fit. // -// TEARDOWN AND CHANGE REFUSE RATHER THAN BEING REALIZED. ADDITION realizes when the address can -// be constructed: directories always, the slice only when the slot CPU quota survey resolved. -// A refused survey is CellCpuQuotaSurveyRefused so an absent cell cannot be created without a -// CPU ceiling. +// TEARDOWN AND CHANGE REFUSE RATHER THAN BEING REALIZED. ADDITION always realizes: directories +// by path, the slice by the six directives of fabric_cell_slice_desired_directives_for. // Teardown of a cell root is a DESTRUCTIVE effect on a directory that may hold live // attempt state, and change realization is gunbc.change_realization's question -- it classifies a // MemberChanged by whether the runtime address survived, and this family has not yet declared its @@ -241,21 +227,11 @@ fn fabric_cell_boundary_add_realization( slot: RunnerSlotIdentity, quota: RunnerSlotCpuQuota, ) -> FabricCellAddressRealization { - match fabric_cell_slice_desired_directives_for(quota: quota) { - SliceDirectivesComplete { directives: ds } => - AddressRealizable { - effect: FabricCellSliceEnsure { - slice_unit: fabric_cell_slice_name(slot: slot) as NonEmptyStr, - directives: ds, - }, - } - SliceDirectivesQuotaSurveyRefused { cause: c } => - AddressRefused { - refusal: CellCpuQuotaSurveyRefused { - address_wire: fabric_cell_resource_address_wire(address: FabricCellResourceBoundaryAddress), - cause: c, - }, - } + AddressRealizable { + effect: FabricCellSliceEnsure { + slice_unit: fabric_cell_slice_name(slot: slot) as NonEmptyStr, + directives: fabric_cell_slice_desired_directives_for(quota: quota), + }, } } diff --git a/dag/gunbc/fleet/fleet_converge_plan.dag b/dag/gunbc/fleet/fleet_converge_plan.dag index 626ca892d98..efc050ecc32 100644 --- a/dag/gunbc/fleet/fleet_converge_plan.dag +++ b/dag/gunbc/fleet/fleet_converge_plan.dag @@ -67,7 +67,6 @@ import gunbc.fabric_cell_effect { CellRosterDuplicateKey, CellChangeNotYetRealizable, CellTeardownNotYetRealizable, - CellCpuQuotaSurveyRefused, CellPlanRealizable, CellPlanRefused, FabricCellEffect, @@ -1103,8 +1102,6 @@ fn fabric_cell_realization_refusal_line(refusal: FabricCellRealizationRefusal) - concat("fabric-cell ", concat(w, " change realization undeclared: no staged intermediates (gunbc.change_realization)")) CellTeardownNotYetRealizable { address_wire: w } => concat("fabric-cell ", concat(w, " teardown unmodeled: destructive effect over possibly-live attempt state")) - CellCpuQuotaSurveyRefused { address_wire: w, cause: c } => - join(["fabric-cell ", w, " CPU quota survey refused: ", c], "") } } diff --git a/dag/gunbc/guarantee_stall/runner_microvm_guest_size_derivation_stall.dag b/dag/gunbc/guarantee_stall/runner_microvm_guest_size_derivation_stall.dag index 144a83a4ff8..e4b2e272ddc 100644 --- a/dag/gunbc/guarantee_stall/runner_microvm_guest_size_derivation_stall.dag +++ b/dag/gunbc/guarantee_stall/runner_microvm_guest_size_derivation_stall.dag @@ -23,9 +23,10 @@ import gunbc.guarantee_stall { GuaranteeStall, AwaitsOneGrounding, BoundedPopula // MEMBERSHIP RULE: a symbol is a member when, IN PRODUCTION, it cannot reach its intended answer // because of the missing memory quantity, or because guest topology is still the withdrawn // declaration rather than a derivation from the cell's absolute CPU ceiling. gunbc_runner_microvm_ -// cell_cpu_entitlement is NOT a member: it now returns CpuEntitlementAbsolute from the slot quota -// when the survey resolves. fabric_cell_slice_desired_directives is NOT a member: it emits -// CPUQuota when the slot declares one. guest_memory_fits remains a member because it still +// cell_cpu_entitlement is NOT a member: it reads the slot quota and answers RelativeOnly under the +// 2026-09-19 no-ceiling policy, which is its intended answer for that row rather than a stall. +// fabric_cell_slice_desired_directives is NOT a member: it emits the CPUQuota line the slot +// declares (a ceiling or the affirmative unset). guest_memory_fits remains a member because it still // returns MemoryFitUnresolved on the production path. // // THE TRIGGER CARRIES TWO CLAUSES AND BOTH ARE REQUIRED, because satisfying the first alone would diff --git a/dag/gunbc/host/host_converge.dag b/dag/gunbc/host/host_converge.dag index 3f359874530..135b3d48834 100644 --- a/dag/gunbc/host/host_converge.dag +++ b/dag/gunbc/host/host_converge.dag @@ -16,7 +16,7 @@ import gunbc.ci_runner_placement { RunnerDeploymentPlan, HostRunnerDeploymentDerived, HostRunnerDeploymentRefused, RunnerHostDeployment, - RunnerCpuBoundary, CapacityQuota, + RunnerCpuBoundary, CapacityQuota, NoCpuCeiling, runner_deployment_plan, workload_class_cpu_weight, WorkloadClass, @@ -42,7 +42,7 @@ import extdeps.systemd { systemd_cpu_quota_property, systemd_tasks_max_property, } -import extdeps.systemd.unit_file { systemd_directive_value_wire, DirectiveCpuTime } +import extdeps.systemd.unit_file { systemd_directive_value_wire, SystemdDirectiveValue, DirectiveCpuTime, DirectiveReset } import extdeps.systemd.oomd { oomd_managed_oom_memory_pressure_property, oomd_managed_oom_memory_pressure_limit_property, @@ -554,25 +554,32 @@ fn codex_runtime_bundle_knobs(host: HostIdentity) -> List { // does not pre-empt that rename; it is the third value to arrive at a field still named // bytes_value, which strengthens the case that lane already carries rather than adding a new one. // -// EVERY REPRESENTABLE BOUNDARY EMITS A KNOB. RunnerCpuBoundary has exactly one constructor, so this -// function is total without an arm that converges successfully while realizing nothing -- the -// fail-open review 57675 found, now closed by deletion at the type rather than by a refusal here. +// EVERY REPRESENTABLE BOUNDARY EMITS A KNOB, INCLUDING THE ONE THAT MEANS NO CEILING. An arm that +// converges successfully while realizing nothing is the fail-open review 57675 found; NoCpuCeiling +// is not that arm, because "no ceiling" is a state a unit can fail to be in (one that was once +// given a quota keeps it), so it is realized as the affirmative empty assignment `CPUQuota=` that +// systemd reads as unset, through the same drop-in the ceiling would use. fn runner_cpu_boundary_knobs(h: RunnerHostDeployment, glob: String) -> List { - match h.per_runner_cpu_boundary { - CapacityQuota { threads: t } => - [ - legacy_converge_knob( - slice: RunnerSlice, - name: "per_slot_cpu_quota", - desired: systemd_directive_value_wire(value: DirectiveCpuTime { threads: t }), - target: PerSlotMemoryCap { - dropin_path: gunbc_runner_per_slot_cpu_dropin_path, - instance_glob: glob, - property: systemd_cpu_quota_property, - bytes_value: systemd_directive_value_wire(value: DirectiveCpuTime { threads: t }), - } - ), - ] + let value = runner_cpu_boundary_directive_value(b: h.per_runner_cpu_boundary) + [ + legacy_converge_knob( + slice: RunnerSlice, + name: "per_slot_cpu_quota", + desired: systemd_directive_value_wire(value: value), + target: PerSlotMemoryCap { + dropin_path: gunbc_runner_per_slot_cpu_dropin_path, + instance_glob: glob, + property: systemd_cpu_quota_property, + bytes_value: systemd_directive_value_wire(value: value), + } + ), + ] +} + +fn runner_cpu_boundary_directive_value(b: RunnerCpuBoundary) -> SystemdDirectiveValue { + match b { + CapacityQuota { threads: t } => DirectiveCpuTime { threads: t } + NoCpuCeiling => DirectiveReset } } diff --git a/dag/gunbc/runner/runner_deploy_emit.dag b/dag/gunbc/runner/runner_deploy_emit.dag index 42b6408f379..22cd6229c1f 100644 --- a/dag/gunbc/runner/runner_deploy_emit.dag +++ b/dag/gunbc/runner/runner_deploy_emit.dag @@ -7,11 +7,11 @@ import gunbc.ci_runner_placement { PrimaryCpuUnavailable, UsableRamUnavailable, SessionReservationUnestablished, FixedOverheadUnavailable, CompilePoolUnavailable, HostAllocationOversubscribedOnHost, HostAllocationUnchargeableOnHost, - ZeroRunnerWidth, JobserverCitationAbsent, HostCpuAllocationUnchargeable, RunnerCoreRatioUnresolved, HostNotEnrolledInFleet, + ZeroRunnerWidth, JobserverCitationAbsent, HostCpuAllocationUnchargeable, HostNotEnrolledInFleet, FleetDeploymentComplete, FleetDeploymentIncomplete, fleet_deployment_completion, host_deployment_refusal_text, RunnerHostDeployment, - RunnerCpuBoundary, CapacityQuota, + RunnerCpuBoundary, CapacityQuota, NoCpuCeiling, runner_deployment_plan, SessionReservationPlan, SessionReservationDerived, @@ -35,7 +35,7 @@ import gunbc.session_reservation_policy { import gunbc.ci_fleet { fleet_intent_offers } import gunbc.oomd_install { gunbc_oomd_sessions_policy } import extdeps.github.actions { RunnerSpec, SelfHosted, HostedRunner, RunsOnExpression } -import extdeps.systemd.unit_file { systemd_directive_value_wire, DirectiveCpuTime } +import extdeps.systemd.unit_file { systemd_directive_value_wire, DirectiveCpuTime, DirectiveReset } import std.measure { byte_size_count, hardware_thread_count_value, percent_count } import std.types { String, List, Bool } import std.layout { @@ -74,6 +74,7 @@ fn runner_labels_text(spec: RunnerSpec) -> String { fn runner_cpu_boundary_text(b: RunnerCpuBoundary) -> String { match b { CapacityQuota { threads: t } => systemd_directive_value_wire(value: DirectiveCpuTime { threads: t }) + NoCpuCeiling => systemd_directive_value_wire(value: DirectiveReset) } } diff --git a/dag/gunbc/runner/runner_host_grants.dag b/dag/gunbc/runner/runner_host_grants.dag index e38ed41e3d5..3ecf9f18489 100644 --- a/dag/gunbc/runner/runner_host_grants.dag +++ b/dag/gunbc/runner/runner_host_grants.dag @@ -38,8 +38,6 @@ import gunbc.fabric_cell_effect { CellSubstrateRootOwned, CellAttemptWriterOwned, fabric_cell_slice_desired_directives, - SliceDirectivesComplete, - SliceDirectivesQuotaSurveyRefused, } import gunbc.fabric_allocation_store_substrate { AllocationStoreEffect, @@ -148,19 +146,14 @@ fn fabric_cell_slice_property_operations( ) } -// A REFUSED SURVEY EMITS NO SLICE PROPERTY GRANTS. Applying MemoryMax without CPUQuota is the -// unbounded-CPU slice this module must not authorize. When the survey resolves, the operations -// are `fabric_cell_slice_desired_directives`'s complete list, including `slice_cpu_quota`. +// THE SLICE PROPERTY GRANTS ARE `fabric_cell_slice_desired_directives`'S COMPLETE LIST, six lines +// including the CPUQuota line -- a ceiling or the affirmative unset, never omitted. The fold has no +// refusing arm since the quota's only refusing producer (a vendor survey) was deleted. fn fabric_cell_slice_operations(slot: RunnerSlotIdentity) -> List { - match fabric_cell_slice_desired_directives() { - SliceDirectivesComplete { directives: ds } => - fabric_cell_slice_property_operations( - unit: fabric_cell_slice_name(slot: slot) as NonEmptyStr, - directives: ds, - ) - SliceDirectivesQuotaSurveyRefused { cause: _ } => - [] as List - } + fabric_cell_slice_property_operations( + unit: fabric_cell_slice_name(slot: slot) as NonEmptyStr, + directives: fabric_cell_slice_desired_directives(), + ) } // THE EFFECT-TO-OPERATION PROJECTION THE APPLY SHELL CONSUMES. gunbc.fleet_converge_plan renders diff --git a/dag/gunbc/runner/runner_microvm.dag b/dag/gunbc/runner/runner_microvm.dag index bc1bcf348bd..c1825fb0c8b 100644 --- a/dag/gunbc/runner/runner_microvm.dag +++ b/dag/gunbc/runner/runner_microvm.dag @@ -25,12 +25,8 @@ import product.fabric.isolation { } import gunbc.runner_slot_allocation { gunbc_runner_slot_desired, - gunbc_runner_cores_per_slot, - RunnerCoresPerSlotResolution, - CoresPerSlotResolved, - CoresPerSlotUnresolved, CpuQuotaResolved, - CpuQuotaRefused, + SlotCpuQuotaUnbounded, } import gunbc.runner_unit { RunnerUnitLifecycleIntent, @@ -313,13 +309,15 @@ fn gunbc_runner_microvm_realization_reserve() -> RealizationReserve { } } -// THE CPU AXIS IS THE CELL'S SURVEYED QUOTA, READ HERE RATHER THAN RESTATED. +// THE CPU AXIS IS THE CELL'S DECLARED QUOTA, READ HERE RATHER THAN RESTATED. // -// On memory the quantity may exist and merely be uncited. On CPU the cell now emits CPUQuota= when -// gunbc_runner_slot_cpu_quota resolves, and fabric_cell_acquire reads CPUQuotaPerSecUSec back. -// This function is that same quota seen as a guest-sizing entitlement: Resolved is Absolute, -// Refused is RelativeOnly carrying the survey cause -- never a second invented ceiling, and never -// the CPUWeight-only story that was true before the slice directive existed. +// On memory the quantity may exist and merely be uncited. On CPU the cell emits the CPUQuota= line +// gunbc_runner_slot_cpu_quota declares, and fabric_cell_acquire reads CPUQuotaPerSecUSec back. +// This function is that same quota seen as a guest-sizing entitlement: Resolved is Absolute; +// SlotCpuQuotaUnbounded is RelativeOnly, and its cause names the 2026-09-19 policy that put the +// row there -- never a second invented ceiling. Since that ruling the production path IS +// RelativeOnly: the slot carries CPUWeight and no ceiling, so the declared guest size below is +// the answer served rather than a withdrawn one. // // THE DECLARED PRODUCT SIZE WITHDRAWS THE MOMENT THIS RETURNS ABSOLUTE. That is // runner_microvm_size_decision's job: a stale declaration that kept succeeding would hide that @@ -332,15 +330,18 @@ type CellCpuEntitlement fn gunbc_runner_microvm_cell_cpu_entitlement() -> CellCpuEntitlement { match gunbc_runner_slot_desired().cpu_quota { CpuQuotaResolved { threads: t } => CpuEntitlementAbsolute { threads: t } - CpuQuotaRefused { cause: c } => CpuEntitlementRelativeOnly { cause: c } + SlotCpuQuotaUnbounded => + CpuEntitlementRelativeOnly { + cause: "the slot row declares no absolute CPU ceiling (gunbc.runner_slot_allocation gunbc_runner_slot_cpu_quota, operator ruling 2026-09-19), so the cell grants a relative share only" + } } } // THE GUEST'S TOPOLOGY WAS A DECLARED PRODUCT DECISION WHILE THE CELL GRANTED NO ABSOLUTE // ENTITLEMENT. That declaration withdraws the moment gunbc_runner_microvm_cell_cpu_entitlement -// returns Absolute -- which is now the production path when the slot survey resolves. The numbers -// below remain the srv4 MVP observation, kept as the declared row the RelativeOnly arm still -// serves, not as a sizing anyone may provision against once Absolute is inhabited. +// returns Absolute -- which it does only if the slot row ever declares a CpuQuotaResolved ceiling +// again; under the 2026-09-19 no-ceiling policy the production path is RelativeOnly. The numbers +// below remain the srv4 MVP observation, kept as the declared row the RelativeOnly arm serves. data gunbc_runner_microvm_declared_size: RunnerMicroVmSize = RunnerMicroVmSize { vcpu_count: hardware_thread_count(count: 4), memory: mebibyte(count: 4096), diff --git a/dag/gunbc/runner/runner_slot_allocation.dag b/dag/gunbc/runner/runner_slot_allocation.dag index 2509f99bc62..9ccb7c711d4 100644 --- a/dag/gunbc/runner/runner_slot_allocation.dag +++ b/dag/gunbc/runner/runner_slot_allocation.dag @@ -18,23 +18,15 @@ import gunbc.session_reservation_policy { } import gunbc.ci_failure_class { FloorOutcome, NoFailure, FloorFailed, Structural, floor_outcome_blocks_merge } import extdeps.cpu.ampere { altra_max_m12830_catalog } -import extdeps.toolchain.types { Aarch64 } -import extdeps.ci_runner.types { QuotedBasisUnresolved, ResolvedBytes, quoted_magnitude_bytes } -import extdeps.cloud.ubicloud { ubicloud_provider_identity } -import gunbc.runner_shape_census { - memory_per_vcpu, - surveyed_runner_shapes, - MemoryPerVcpuReading, - UniformMemoryPerVcpu, - RatioNotUniformAcrossSizes, - NonIntegralRatioInSizes, - UnitLabelNotUniformAcrossSizes, - NoShapesForProviderArchitecture, -} +// THE SLOT'S CPU CEILING IS A POLICY OF THE ROW, AND THE ROW'S POLICY IS NO CEILING. CpuQuotaResolved +// is an absolute CPUQuota= of that many threads; SlotCpuQuotaUnbounded is the affirmative declaration +// that the slot carries no absolute ceiling and is governed by cpu_weight alone. There is no refusing +// arm: the value used to be derived from a vendor survey that could fail to resolve, and that survey +// is deleted (gunbc_runner_cores_per_slot_policy), so nothing upstream can refuse. type RunnerSlotCpuQuota = CpuQuotaResolved { threads: HardwareThreadCount } - | CpuQuotaRefused { cause: String } + | SlotCpuQuotaUnbounded type RunnerSlotLabels { github_labels: List @@ -58,7 +50,7 @@ type HostRunnerSlotAllocation { } data gunbc_runner_slot_allocation_authority_disposition: Disposition = Terminal { - reason: "Single authority for fleet runner-slot desired allocation (operator rulings 2026-07-10 superseding the 2026-07-05 brief; MemoryHigh-emit + swap-provisioning 2026-07-11; srv3 host-standup converge 2026-07-21; PER-HOST WIDTH + 14GiB slot 2026-08-05; srv4 disk row-pin retired + apply-time preflight 2026-08-06). Fleet-uniform per-slot row: 6 cores (DERIVED 2026-08-23 from the 16GiB MemoryMax at the surveyed 1 core : 3 GiB memory-per-vCPU convention, superseding an authored 8 that stated no relation to the ceiling beside it -- gunbc_runner_memory_per_core reads it from gunbc.runner_shape_census rather than restating it as a literal here) · 16GiB MemoryMax · 15GiB MemoryHigh (the authored max−1GiB relation, its own emitted knob per the 2026-07-11 ruling — a hit at 16GiB spills to swap after throttling at 15; raised from 14/13 by the 2026-08-17 floor-peak ruling in gunbc_runner_slot_memory_max_ruling_note) · 32GiB MemorySwapMax · TasksMax 16384 (the 2026-06-21 incident REPAIR value restored by operator ruling 2026-08-08 on PR 8045: TasksMax=512 WAS the diagnosed EAGAIN/process-creation outage value, and the earlier 512 row here inverted that diagnosis; any lower derived envelope value must be separately accepted and never below live task demand) · cpu_weight 100. PER-HOST width, RE-DERIVED 2026-08-27: MEMORY admits 25 on srv1 and 29 on srv3/srv4 at the 16GiB slot, and on srv2 IT IS UNESTABLISHED. CPU admits 21 on every host (128 cited cores at 6 per slot); disk admits nothing numeric anywhere. COMMITTED width is the minimum over those axes. SUPERSEDED 2026-09-13 BY THE 26/25 GiB TRANSITION: the slot is 26 GiB MemoryMax / 25 GiB MemoryHigh, cores_per_slot rises 6 -> 10, and CPU admits 12 on every host (128 cited cores at 10 per slot), so srv1/srv3/srv4 commit 12 and the fleet commits 36. srv2 NO LONGER HOLDS 5 AND HOLDS NOTHING: the held arm is deleted, its width RESOLVES TO A TYPED REFUSAL, and `gunbc.runner_host_deploy` constructs no operational deploy row for it at all. The figures in the rest of this row that describe the 16/15 slot and the 21-wide commitment are the PRIOR configuration and are retained only as the transition's starting point -- they are not the current standing and must not be cited as one.\n\nsrv1 MOVED 27 -> 25 AND THAT IS A SYNCHRONIZATION, NOT A NEW TARGET. The 27 was consistent with an ungrounded 20 GiB fleet-wide session figure; that figure is deleted and srv1 carries an evidence-backed 52 GiB allowance, which removes three slots from the derivation this literal claims to report. No committed width moves, because srv1's CPU axis binds at 21 either way.\n\nsrv2's HELD 5 IS DELETED (2026-09-13) AND WHAT REPLACED IT IS THE TYPED REFUSAL THIS NOTE ASKED FOR. The paragraph below states the eventual construction fix as replacing the Int API with a typed standing so an unestablished host cannot be read as a number at all; that is now done at the DEPLOY seam -- `RunnerHostDeployAdmission` constructs an operational row only in the admitted arm, so srv2 yields a cause and no row, and zero can no longer inhabit `RunnerHostDeploy`. The historical argument is kept because it is what motivated the construction; the held number it defends no longer exists. Its session reservation is unestablished, so the memory subtraction cannot be performed at all -- there is no denominator to subtract. A held number and a derived one are different claims: the first says what the host is currently running while it is re-grounded, the second says what the conservation wall would pass, and only the second is evidence. The previous revision of this sentence listed 5 beside three derived figures as though the four were the same kind of fact, which is exactly the reading the ruling forbids. test.claim.host_allocation_conservation the_srv2_width_derivation_is_unestablished is the executed statement of this; every_derived_width_matches_its_authored_copy deliberately excludes srv2 rather than counting it as reproduced. The eventual construction fix is to replace this Int API with a typed standing (MemoryWidthDerived / MemoryWidthUnestablished) so an unestablished host cannot be read as a number at all; until then this note is the boundary and the two witnesses are the wall. THE BINDING AXIS CHANGED AND THAT IS THE HEADLINE: memory bound every host in this fleet's entire history until 2026-08-22, and now binds only srv2, the one host whose 64GiB install failed training and was reverted. Everywhere else CPU binds, which is why host_memory_admitted_width reporting 29 is not a commitment to 29 (width_minimum_now_spans_three_axes_note). srv4's disk stays DiskWidthUnconstrained at allocation with the apply-time gate gunbc.runner_lifecycle runner_width_disk_preflight unchanged. Live fleet ran 5 slots on EVERY host when this was written, so srv3/srv4's sixth slot is a provisioning target rather than a description (gunbc_runner_slot_width_ruling_note). GUARANTEED mode is now STRUCTURAL rather than checked — runner_slice_cap is DERIVED as slots × MemoryMax instead of being a second literal, so Σ per-slot MemoryMax ≤ slice_cap holds by construction and an oversubscribed slice has no representation here (DESIGN 5 construction-over-validation; the prior 80GiB literal was kept coherent with 5 × 16GiB by hand, which the old note admitted when it called them 'coherent by construction at these values' — a coincidence maintained by an author, not a derivation). The real remaining constraint is CONSERVATION against host RAM, which is not this module's to check: gunbc.ci_runner_placement holds it, now against OS-usable RAM with the per-host session slice and per-host fixed overhead charged. Per-axis targets are operator-set; never reconcile this row toward live readback (live is not automatically truth — the 32-vs-0 swap fork existed precisely because the apply was assumed rather than readback-proven). Downstream: gunbc.ci_floor_measurement (declared vs live cap fork), gunbc.ci_runner_placement, gunbc.host_converge, gunbc.host_standup (prefix:host-converge + P1:runner-deploy-slot), gunbc.runner_slot_enforcement, gunbc.fleet_show_effective_read. ctrl applies host-side; gunbc emits converge — never apply from this module." + reason: "RE-DERIVED 2026-09-19 UNDER THE OPERATOR RULING 'there is no CPU demand - the processes we run are single core - do NOT overthink it please': cores_per_slot is an AUTHORED POLICY of 1 (gunbc_runner_cores_per_slot_policy), no longer derived from MemoryMax against a vendor memory-per-vCPU ratio, so the CPU axis admits 128 on every M128-30 host and STOPS BINDING; committed width is the memory-admitted width -- srv1 15, srv3/srv4 17 at the 26 GiB slot, fleet 49; and cpu_quota is SlotCpuQuotaUnbounded, so no CPUQuota= is rendered for a runner unit or a fabric cell slice (the hosts run unlimited today and a 100% quota would throttle parallel link/codegen). Every figure below that names a cores_per_slot of 6 or 10, a CPU admission of 21 or 12, or a CPU-bound committed width is PRIOR configuration retained as history.\n\nSingle authority for fleet runner-slot desired allocation (operator rulings 2026-07-10 superseding the 2026-07-05 brief; MemoryHigh-emit + swap-provisioning 2026-07-11; srv3 host-standup converge 2026-07-21; PER-HOST WIDTH + 14GiB slot 2026-08-05; srv4 disk row-pin retired + apply-time preflight 2026-08-06). Fleet-uniform per-slot row: 6 cores (DERIVED 2026-08-23 from the 16GiB MemoryMax at the surveyed 1 core : 3 GiB memory-per-vCPU convention, superseding an authored 8 that stated no relation to the ceiling beside it -- gunbc_runner_memory_per_core reads it from gunbc.runner_shape_census rather than restating it as a literal here) · 16GiB MemoryMax · 15GiB MemoryHigh (the authored max−1GiB relation, its own emitted knob per the 2026-07-11 ruling — a hit at 16GiB spills to swap after throttling at 15; raised from 14/13 by the 2026-08-17 floor-peak ruling in gunbc_runner_slot_memory_max_ruling_note) · 32GiB MemorySwapMax · TasksMax 16384 (the 2026-06-21 incident REPAIR value restored by operator ruling 2026-08-08 on PR 8045: TasksMax=512 WAS the diagnosed EAGAIN/process-creation outage value, and the earlier 512 row here inverted that diagnosis; any lower derived envelope value must be separately accepted and never below live task demand) · cpu_weight 100. PER-HOST width, RE-DERIVED 2026-08-27: MEMORY admits 25 on srv1 and 29 on srv3/srv4 at the 16GiB slot, and on srv2 IT IS UNESTABLISHED. CPU admits 21 on every host (128 cited cores at 6 per slot); disk admits nothing numeric anywhere. COMMITTED width is the minimum over those axes. SUPERSEDED 2026-09-13 BY THE 26/25 GiB TRANSITION: the slot is 26 GiB MemoryMax / 25 GiB MemoryHigh, cores_per_slot rises 6 -> 10, and CPU admits 12 on every host (128 cited cores at 10 per slot), so srv1/srv3/srv4 commit 12 and the fleet commits 36. srv2 NO LONGER HOLDS 5 AND HOLDS NOTHING: the held arm is deleted, its width RESOLVES TO A TYPED REFUSAL, and `gunbc.runner_host_deploy` constructs no operational deploy row for it at all. The figures in the rest of this row that describe the 16/15 slot and the 21-wide commitment are the PRIOR configuration and are retained only as the transition's starting point -- they are not the current standing and must not be cited as one.\n\nsrv1 MOVED 27 -> 25 AND THAT IS A SYNCHRONIZATION, NOT A NEW TARGET. The 27 was consistent with an ungrounded 20 GiB fleet-wide session figure; that figure is deleted and srv1 carries an evidence-backed 52 GiB allowance, which removes three slots from the derivation this literal claims to report. No committed width moves, because srv1's CPU axis binds at 21 either way.\n\nsrv2's HELD 5 IS DELETED (2026-09-13) AND WHAT REPLACED IT IS THE TYPED REFUSAL THIS NOTE ASKED FOR. The paragraph below states the eventual construction fix as replacing the Int API with a typed standing so an unestablished host cannot be read as a number at all; that is now done at the DEPLOY seam -- `RunnerHostDeployAdmission` constructs an operational row only in the admitted arm, so srv2 yields a cause and no row, and zero can no longer inhabit `RunnerHostDeploy`. The historical argument is kept because it is what motivated the construction; the held number it defends no longer exists. Its session reservation is unestablished, so the memory subtraction cannot be performed at all -- there is no denominator to subtract. A held number and a derived one are different claims: the first says what the host is currently running while it is re-grounded, the second says what the conservation wall would pass, and only the second is evidence. The previous revision of this sentence listed 5 beside three derived figures as though the four were the same kind of fact, which is exactly the reading the ruling forbids. test.claim.host_allocation_conservation the_srv2_width_derivation_is_unestablished is the executed statement of this; every_derived_width_matches_its_authored_copy deliberately excludes srv2 rather than counting it as reproduced. The eventual construction fix is to replace this Int API with a typed standing (MemoryWidthDerived / MemoryWidthUnestablished) so an unestablished host cannot be read as a number at all; until then this note is the boundary and the two witnesses are the wall. THE BINDING AXIS CHANGED AND THAT IS THE HEADLINE: memory bound every host in this fleet's entire history until 2026-08-22, and now binds only srv2, the one host whose 64GiB install failed training and was reverted. Everywhere else CPU binds, which is why host_memory_admitted_width reporting 29 is not a commitment to 29 (width_minimum_now_spans_three_axes_note). srv4's disk stays DiskWidthUnconstrained at allocation with the apply-time gate gunbc.runner_lifecycle runner_width_disk_preflight unchanged. Live fleet ran 5 slots on EVERY host when this was written, so srv3/srv4's sixth slot is a provisioning target rather than a description (gunbc_runner_slot_width_ruling_note). GUARANTEED mode is now STRUCTURAL rather than checked — runner_slice_cap is DERIVED as slots × MemoryMax instead of being a second literal, so Σ per-slot MemoryMax ≤ slice_cap holds by construction and an oversubscribed slice has no representation here (DESIGN 5 construction-over-validation; the prior 80GiB literal was kept coherent with 5 × 16GiB by hand, which the old note admitted when it called them 'coherent by construction at these values' — a coincidence maintained by an author, not a derivation). The real remaining constraint is CONSERVATION against host RAM, which is not this module's to check: gunbc.ci_runner_placement holds it, now against OS-usable RAM with the per-host session slice and per-host fixed overhead charged. Per-axis targets are operator-set; never reconcile this row toward live readback (live is not automatically truth — the 32-vs-0 swap fork existed precisely because the apply was assumed rather than readback-proven). Downstream: gunbc.ci_floor_measurement (declared vs live cap fork), gunbc.ci_runner_placement, gunbc.host_converge, gunbc.host_standup (prefix:host-converge + P1:runner-deploy-slot), gunbc.runner_slot_enforcement, gunbc.fleet_show_effective_read. ctrl applies host-side; gunbc emits converge — never apply from this module." } data gunbc_runner_slot_memory_max_ruling_note: String = "WHY 16GiB/15GiB, SUPERSEDING THE 14GiB/13GiB ROW (operator ruling 2026-08-17). The 2026-08-05 derivation below is not repudiated — it is re-run on a changed input. Its subject is floor-job cgroup peaks, and the floor's peak moved: the claim-scope cut measures 15.6GB (14.53GiB) after taking whole-run peak from 45.4GB to 15.6GB. 14.53GiB does not fit a 14GiB MemoryMax and sits 1.53GiB above a 13GiB MemoryHigh, so the prior row can no longer hold its own consumer.\n\nTHE PIN IS OBSERVED, NOT PREDICTED (srv1, read 2026-08-17, gunbc_srv1_converged_slot_throttle_observation). All five srv1 slots carry the declared 14GiB/13GiB. Two of them — srv1-04 and srv1-05 — report memory.peak of EXACTLY 13.00GiB against 829 and 44803 memory.high events, with memory.max events and oom_kill both zero. A peak equal to the throttle line is the censored lower bound this very note taught for the four 15.0GiB pins in the 2026-08-05 sample; it is the workload being held, not the workload being measured. Survival without OOM is reclaim plus swap, and the host carried 10GiB of swap in use at the same reading.\n\nWHAT IT COSTS, STATED BECAUSE IT IS NOT FREE: two runner slots fleet-wide. At 16GiB the managed hosts admit 6 rather than 7 (7 x 16 = 112GiB against 111.4GiB available) while srv1/srv2 hold 5 (5 x 16 = 80GiB against 81.0GiB, a 1.0GiB margin — the tightest number in this module and the first thing to recheck if the sessions reservation or the legacy overhead is ever corrected upward). Fleet total 24 -> 22. The trade is bought deliberately: a 15GiB/14GiB row would have preserved all 24 slots and cleared MemoryMax, but its throttle line would sit 0.53GiB BELOW the measured peak, making the throttle confound permanent and structural in the instrument now measuring the fold.\n\nTHAT THE NEW TARGET EQUALS srv2-03'S DRIFTED READBACK IS A COINCIDENCE AND MUST NOT BE READ AS A PRECEDENT. gunbc_runner_slot_allocation_refuses_effective_readback_reconciliation still stands unamended: this row moved because a floor-peak measurement moved, which is the same subject the 2026-08-05 ruling used, and it would have moved to 16GiB had every host in the fleet been sitting at 8. srv1 was CONVERGED to 14/13 when this was written, so the fleet was not at 16/15 to be reconciled toward.\n\nDISSOLVE-ON: the 14.53GiB peak is not established as steady. If the fold's remaining 5.6GB comes down, re-derive from the new floor-peak distribution and reclaim the two slots; this row is sized to a measurement under active reduction, not to a settled envelope.\n\nSUPERSEDED DERIVATION, RETAINED AS THE PRIOR INPUT (operator ruling 2026-08-05). 186 floor-job cgroup peaks from 2026-08-05 plus five sampled days back to 2026-07-16, read from the [calibration] floor_peak_post receipts the ci job emits: successful runs peak at p50 10.32GiB and max 10.96GiB, and NOT ONE run in the sample reached the 16GiB MemoryMax. Four of 186 reached 15.0GiB, which is the 15GiB MemoryHigh exactly — those are throttle pins, censored lower bounds rather than demand, and they are why the cap does not fall further.\n\nTHE FLOOR UNDER IT IS ALREADY MEASURED AND IT IS 12GiB: gunbc_floor_minimum_viable_armed_budget in this module, enforced by claim_executor as an arm-time refusal. MemoryHigh is what the run-time governor arms against, so MemoryHigh is the axis that number constrains — 13GiB leaves exactly 1GiB of margin over it, and clears the worst p90 in the three-week sample (12.94GiB on 2026-07-30). Going to 13/12 to buy an eighth slot on the managed hosts would put the throttle line ON the measured refusal floor with zero margin, and the 2026-07-30 distribution says that day would have spent most of its runs in swap.\n\nWHAT MOVING 16 TO 14 IS NOT: it is not oversubscription and does not touch the guaranteed-mode property. Every slot still owns its ceiling outright. The ceilings are simply sized closer to what the workload was measured to want, which is what freed the width." @@ -115,26 +107,14 @@ data gunbc_runner_slot_width_ruling_note: String = "SUPERSEDED 2026-08-23 AND RE // // WHAT IT COSTS, AND BOTH AXES MOVE BY CONSTRUCTION rather than by a second edit. The memory axis // is now the envelope quotient, so srv1 goes 25 -> 15 and srv3/srv4 29 -> 17 on this literal alone. -// THE CPU AXIS MOVES TOO, AND THAT IS THE PART A READER WILL NOT SEE AT THIS LITERAL: -// `gunbc_runner_cores_per_slot` derives cores from THIS ceiling -- -// ceil(memory_max / memory_per_core) against the cited Ubicloud arm64 ratio of 3 GB decimal per -// vCPU -- so 16 -> 26 GiB takes cores_per_slot 6 -> 10 and the CPU axis 128/6 = 21 down to -// 128/10 = 12. Memory admits 15 and 17; CPU admits 12; the minimum is 12. The fleet moves -// 21/21/21 -> 12/12/12 and loses 22 of 63 slots. -// -// AN EARLIER REVISION OF THIS PARAGRAPH SAID 15/17/17 AND 14 SLOTS, having reasoned about the -// memory axis alone. It was caught by -// `runner_slot_allocation_committed_widths_are_the_minimum_over_axes` going red, not by review, and -// the correction is recorded rather than silently applied because the coupling is the durable fact: -// raising the memory ceiling SPENDS CPU WIDTH, at a ratio taken from a cloud vendor's shape catalog -// rather than from any measurement of this workload. The floor run behind this ruling uses about -// one core sustained (cpu 20 min against 24 min wall), so eight of the twenty-two lost slots are -// not bought by the memory this change needed -- they are bought by CPU quota nothing asked for. -// -// THAT IS ACCEPTED HERE RATHER THAN REPAIRED (operator ruling 2026-09-12, presented with the -// alternative of deriving cores_per_slot from measured floor CPU demand and declined for now). It -// is a real cost knowingly taken, not an oversight, and the repair is a separate change to a -// separate authority rather than a rider on a capacity decision. +// THE CPU AXIS NO LONGER MOVES WITH THIS LITERAL (2026-09-19). Until that date +// `gunbc_runner_cores_per_slot` derived cores from THIS ceiling against a vendor memory-per-vCPU +// ratio, so 16 -> 26 GiB took cores_per_slot 6 -> 10 and the CPU axis 128/6 = 21 down to +// 128/10 = 12, binding the fleet at 12/12/12 -- eight of the twenty-two lost slots bought by CPU +// quota nothing asked for, since the floor run uses about one core sustained (cpu 20 min against +// 24 min wall). That coupling was accepted on 2026-09-12 and REPAIRED on 2026-09-19 by the ruling +// recorded at gunbc_runner_cores_per_slot_policy: cores_per_slot is 1, the CPU axis admits 128 and +// does not bind, and the memory admissions 15 and 17 ARE the committed widths. // // The whole trade is bought deliberately: at 16/15 the floor welds to the throttle line on every // run and its verdict is decided by whether the working set happens to drift back under, which is @@ -179,14 +159,14 @@ data gunbc_runner_slot_memory_max_bytes: Int = 27917287424 data gunbc_runner_slot_memory_max: ByteSize = byte_size(gunbc_runner_slot_memory_max_bytes) -// THE CPU QUOTA IS THE SURVEYED CORES-PER-SLOT, RESOLVED OR REFUSED. A Resolved arm is an absolute -// ceiling the cell slice can enforce; a Refused arm means the survey failed and this function does -// not map that failure onto a no-ceiling policy state. +// THE SLOT CARRIES NO ABSOLUTE CPU CEILING, BY THE SAME RULING THAT SETS cores_per_slot TO ONE. +// This used to be CpuQuotaResolved at the derived cores_per_slot, which with the policy at 1 would +// render CPUQuota=100% on every runner unit and fabric cell slice -- a ceiling that throttles the +// parallel link and codegen phases of the very single-core-sustained workload the ruling describes. +// The hosts run with CPUQuota unlimited today; this arm says so rather than emitting a new +// constraint the ruling did not ask for. cpu_weight stays the relative-share governor. fn gunbc_runner_slot_cpu_quota() -> RunnerSlotCpuQuota { - match gunbc_runner_cores_per_slot() { - CoresPerSlotResolved { threads: t } => CpuQuotaResolved { threads: t } - CoresPerSlotUnresolved { cause: c } => CpuQuotaRefused { cause: c } - } + SlotCpuQuotaUnbounded } fn gunbc_runner_slot_desired() -> RunnerSlotDesired { @@ -281,10 +261,11 @@ fn runner_label_sets_equal(a: List, b: List) -> Bool { // // TWENTY-NINE IS NOT A COMMITMENT AND MUST NOT BE READ AS ONE. This function answers what MEMORY // admits and nothing else; the committed width is gunbc_runner_slots_per_host, which takes the -// minimum over memory, CPU and disk and lands on 21 for srv1/srv3/srv4 because 128 cores at 6 per -// slot is the binding axis now. The CPU axis landed in the SAME change as these literals for -// exactly that reason -- with no CPU term the minimum degenerates to memory alone and 29 would -// commit 29. Neither half of that pair is safe to carry without the other. +// minimum over memory, CPU and disk. When these literals landed it was 21 for srv1/srv3/srv4, +// because 128 cores at 6 per slot was the binding axis; as of 2026-09-19 cores_per_slot is the +// policy of 1, the CPU axis admits 128, and the memory admission IS the commitment on every derived +// host -- by ruling, not by the coincidence that once made this function's answer read as the +// commitment. The minimum still runs over all three axes so that stays a derivation, not a rename. // // THESE ARE STILL AUTHORED LITERALS AND THAT IS STILL DEBT. test.claim.host_allocation_conservation // derived_width_reproduces_every_authored_literal re-runs the arithmetic and joins it to each @@ -404,28 +385,20 @@ fn host_disk_admitted_width(host: HostIdentity) -> HostDiskWidthConstraint { // row, so the uniformity this function relies on is checked somewhere rather than believed here. // // WHAT THIS DOES NOT MODEL, said plainly because the number looks more complete than it is: there -// is no CPU RESERVATION. Every core is offered to runner slots, so 128 cores at 6 per slot admits -// 21 and the host, its kernel, and the interactive session containers on srv1 and srv2 are charged +// is no CPU RESERVATION. Every core is offered to runner slots, so 128 cores at 1 per slot admits +// 128 and the host, its kernel, and the interactive session containers on srv1 and srv2 are charged // nothing on this axis. The memory axis DOES charge them -- a flat fixed overhead on every host // plus a sessions slice on the two hosts that carry sessions -- so the omission is confined to CPU, // where oversubscription degrades throughput rather than triggering the OOM kill that makes memory -// unforgiving. It is a real gap and not a judgement that CPU is free. DISSOLVE-ON: a measured -// host-and-session CPU residency series, at which point this subtracts a reserve exactly as the -// memory axis already does. - -type CpuAdmittedWidth - = CpuAdmitsWidth { slots: Int } - | CpuWidthUnresolved { cause: String } - -fn host_cpu_admitted_width(host: HostIdentity) -> CpuAdmittedWidth { - match gunbc_runner_cores_per_slot() { - CoresPerSlotUnresolved { cause: c } => CpuWidthUnresolved { cause: c } - CoresPerSlotResolved { threads: t } => { - let cores = altra_max_m12830_catalog.cores - let per_slot = hardware_thread_count_value(t: t) - CpuAdmitsWidth { slots: cores / per_slot } - } - } +// unforgiving. Under the 2026-09-19 ruling the axis is expected NOT to bind (the workload is single +// core), so the gap has no width consequence today; it is kept as an axis so a host that is not an +// M128-30, or a future cores_per_slot policy above 1, is charged by construction. +// +// THE AXIS CANNOT REFUSE. It used to carry an unresolved arm for a vendor survey that could fail to +// read; cores_per_slot is now an authored policy, so the only inputs are two cited constants and +// the quotient is total. +fn host_cpu_admitted_width(host: HostIdentity) -> Int { + altra_max_m12830_catalog.cores / hardware_thread_count_value(t: gunbc_runner_cores_per_slot()) } data width_minimum_now_spans_three_axes_note: String = "THE MINIMUM IS OVER THREE AXES AND THE THIRD ONE IS WHY THIS CHANGE IS SAFE. Memory admits 29 on the upgraded hosts, CPU admits 21, disk admits nothing numeric, so the commitment is 21 -- and the two answers coexist under their own names instead of one of them being suppressed to keep the other honest. width_is_a_minimum_over_axes_note records what happens when they are collapsed: gunbc_runner_slots_per_host once returned the memory verdict under a name that read as the full commitment, and srv4's roster and its prose disagreed as a result.\n\nSo host_memory_admitted_width saying 29 is not a claim that 29 runners may exist. It is the true answer to the question that function asks, and it is the minimum's job -- not the literal's -- to keep the fleet from acting on it." @@ -436,10 +409,11 @@ data width_minimum_now_spans_three_axes_note: String = "THE MINIMUM IS OVER THRE // cannot say what would unblock more capacity, and the answer differs per host: where memory binds // the remedy is DIMMs, where cores bind it is a different machine, and the two are unrelated // purchases. A reader given only the total assumes one binding axis across the fleet and plans -// against the wrong one -- which is now the live situation rather than a future one, since the CPU -// axis above makes srv1/srv3/srv4 core-bound while srv2 is bound by no axis at all: its memory -// admission is unmeasured for want of a session denominator, so its width is HELD rather than -// derived, and the remedy there is re-grounding that denominator rather than any purchase. +// against the wrong one. From 2026-08-23 to 2026-09-19 the CPU axis made srv1/srv3/srv4 core-bound; +// under the one-core-per-slot policy memory binds them again, while srv2 is bound by no axis at +// all: its memory admission is unmeasured for want of a session denominator, so its width is +// refused rather than derived, and the remedy there is re-grounding that denominator rather than +// any purchase. // // THE UNMEASURED AXIS IS REPORTED AS UNMEASURED, NOT AS ADMITTING EVERYTHING. Disk currently // returns DiskWidthUnconstrained on every host, and this module's own reason string is explicit @@ -489,11 +463,7 @@ fn host_width_axes(host: HostIdentity) -> List { reason: "session reservation unestablished on this host, so the memory subtraction has no denominator and states no width" as NonEmptyStr, } } - let cpu = match host_cpu_admitted_width(host: host) { - CpuAdmitsWidth { slots: n } => AxisAdmits { axis: "cores" as NonEmptyStr, slots: n } - CpuWidthUnresolved { cause: c } => - AxisUnmeasured { axis: "cores" as NonEmptyStr, reason: c as NonEmptyStr } - } + let cpu = AxisAdmits { axis: "cores" as NonEmptyStr, slots: host_cpu_admitted_width(host: host) } let disk = match host_disk_admitted_width(host: host) { DiskAdmitsWidth { slots: d, provenance: _ } => AxisAdmits { axis: "disk" as NonEmptyStr, slots: d } @@ -623,9 +593,9 @@ fn host_held_runner_width(host: HostIdentity) -> Int? { // so the deletion of that round trip was behaviour-preserving on the current tree and NOT on every // tree the source can express. // -// The derived path would still refuse without this: cores_per_slot ceilings memory_max against the -// surveyed memory-per-vCPU convention, so a zero ceiling yields zero cores per slot. THE HELD PATH -// WOULD NOT. It is consulted +// The derived path would NOT refuse without this: cores_per_slot is an authored policy with no +// relation to memory_max, so a zero ceiling yields a zero memory quotient but a full CPU admission. +// THE HELD PATH WOULD NOT EITHER. It is consulted // first and answers with an authored number that never consults the slot shape at all, so srv2's held // five would survive a slot shape admitting nothing -- committing five runners on a ceiling of zero // bytes. That is the one place the accidental refusal was load-bearing, and it is why the standing is @@ -644,170 +614,60 @@ fn runner_slot_shape_standing(d: RunnerSlotDesired) -> RunnerSlotShapeStanding { } } -// UNKNOWN AND ZERO ARE DIFFERENT ANSWERS, AND THIS FUNCTION STILL CONFLATES THEM. -// -// gunbc_runner_slots_per_host returns Int, so the unresolved arm has nowhere to put its cause and -// answers 0. gunbc.ci_runner_placement no longer depends on that -- it consults the ratio directly -// and refuses with RunnerCoreRatioUnresolved, carrying the reason. -// -// THAT MAKES ONE PATH HONEST AND NOT THE PRODUCTION SURFACE, and an earlier revision of this note -// said "the live refusal path is honest" without the qualifier. Two consumers take the Int -// directly and would read an unresolved ratio as a real zero. gunbc.runner_host_deploy builds four -// deploy rows from it, and that count becomes the materialization target -- so a zero changes the -// desired runner population from the host's admitted roster to an empty one, which -// gunbc.runner_slot_provision then plans against; removal ownership refuses destructive teardown, -// which contains the harm, but the plan is answering the wrong question. gunbc.host_axis_caps uses -// the same Int as the upper bound of the runner-unit roster whose system-control shadows are -// reverted, where a zero produces no reverts and can leave stale effective caps standing. -// -// DECLARED GAP, NOT AN ACCIDENT, and the trigger is the CAPABILITY rather than an artifact: the arm -// dissolves when the canonical width answer is a RunnerWidthResolution -- derived, held, or -// unresolved-with-cause -- and every production call site carries or refuses that cause instead of -// reading an Int. That is a fail-closed cascade, including `data` rows whose declared shape has no -// arm to match on, so it is the whole subject of its own change rather than a rider on this one. -// Landing half of it would mean minting one silent default per un-migrated site, which is the -// defect this branch exists to remove. -// -// THE POPULATION IS NAMED BY ITS PRODUCER RATHER THAN TRANSCRIBED, because an earlier revision of -// this annotation said "sixteen production call sites" and a census taken while planning the -// cascade independently said twelve. Both were wrong, and neither was checkable from where it was -// written. The producer is the call-site read over this symbol -- every non-comment, non-import -// occurrence of gunbc_runner_slots_per_host( under dag/gunbc and src/v2, less this declaration -- -// and a reader who wants the count runs it. DESIGN section 6 states the rule this violated: name -// the instrument, never transcribe its output, for the same reason section 3 cites a symbol rather -// than a line -- a copied number is unreachable from the thing that owns it, so it rots without -// anyone touching either end, and here it rotted twice in opposite directions. +// THE DERIVED WIDTH CAN NO LONGER BE UNRESOLVED. Until 2026-09-19 the CPU axis read a vendor +// survey that could fail, and this function mapped that failure to 0 -- a declared gap whose +// dissolution trigger was a typed RunnerWidthResolution carried to every consumer. The gap +// dissolved from the other side: the only unresolvable input was deleted with the survey path, so +// every axis in the minimum is total and there is no arm left to conflate with zero. The +// zero-memory refusal above is the one remaining zero, and it is a refusal by construction, not a +// default. fn gunbc_runner_slots_per_host(host: HostIdentity) -> Int { match runner_slot_shape_standing(d: gunbc_runner_slot_desired()) { RunnerSlotShapeRefusedZeroMemoryMax => 0 RunnerSlotShapeAdmitted => match host_held_runner_width(host: host) { Present { value: held } => held - Absent => - match gunbc_derived_runner_slots_per_host(host: host) { - DerivedRunnerWidthSlots { slots: n } => n - DerivedRunnerWidthUnresolved { cause: _ } => 0 - } + Absent => gunbc_derived_runner_slots_per_host(host: host) } } } -type DerivedRunnerWidth - = DerivedRunnerWidthSlots { slots: Int } - | DerivedRunnerWidthUnresolved { cause: String } - -fn gunbc_derived_runner_slots_per_host(host: HostIdentity) -> DerivedRunnerWidth { - match host_cpu_admitted_width(host: host) { - CpuWidthUnresolved { cause: c } => DerivedRunnerWidthUnresolved { cause: c } - CpuAdmitsWidth { slots: by_cpu } => { - let by_memory = host_memory_admitted_width(host: host) - let by_mem_cpu = if by_cpu < by_memory { by_cpu } else { by_memory } - DerivedRunnerWidthSlots { - slots: match host_disk_admitted_width(host: host) { - DiskAdmitsWidth { slots: d, provenance: _ } => if d < by_mem_cpu { d } else { by_mem_cpu } - DiskWidthUnconstrained { reason: _ } => by_mem_cpu - } - } - } +fn gunbc_derived_runner_slots_per_host(host: HostIdentity) -> Int { + let by_cpu = host_cpu_admitted_width(host: host) + let by_memory = host_memory_admitted_width(host: host) + let by_mem_cpu = if by_cpu < by_memory { by_cpu } else { by_memory } + match host_disk_admitted_width(host: host) { + DiskAdmitsWidth { slots: d, provenance: _ } => if d < by_mem_cpu { d } else { by_mem_cpu } + DiskWidthUnconstrained { reason: _ } => by_mem_cpu } } -// THE MEMORY-PER-VCPU CONVENTION IS SURVEYED, NOT AUTHORED. It replaces a hand-written -// byte_size(3000000000) that restated one provider's published figure as a gunbc literal -- -// a second authority for a number extdeps already cites, which could drift from the catalog -// it was copied from with nothing to notice. -// -// GITHUB ACTIONS IS DELIBERATELY NOT A SECOND SOURCE FOR THE RATIO. Its aarch64 catalog reads -// 4 per vCPU where uniform and carries two sizes that are not integral at all (arm-5core at -// 14, arm-64core at 208), so memory_per_vcpu REFUSES on it rather than reporting a number. -// A provider that aims at a ratio has not published one, and averaging the survey would mint -// a figure no vendor states. Ubicloud aarch64 is the uniform integral reading, so it is the -// one adopted; the survey's disagreement is a typed refusal on the other providers, not noise -// to be smoothed away. -// -// THE BASIS IS THE VENDOR'S AND THE CENSUS NOW CARRIES IT. An earlier revision of this block -// asserted that no surveyed page states whether its "GB" is decimal or binary and then adopted -// GIBIBYTES on that basis. Both halves were wrong: extdeps.cloud.ubicloud quotes its catalog as -// QuotedDecimalGigabytes, so the basis was stated all along and the census was discarding it, and -// the adoption cited an operator ruling that does not exist. The conversion is now the extdeps -// authority's own quoted_magnitude_bytes, which refuses rather than guessing when a sample really -// has no stated basis. -fn gunbc_runner_memory_per_core_reading() -> MemoryPerVcpuReading { - memory_per_vcpu( - rows: surveyed_runner_shapes(), - provider: ubicloud_provider_identity, - architecture: Aarch64, - ) -} - -// AN UNGROUNDED RATIO REFUSES BY NAME INSTEAD OF ANSWERING ZERO. The shape this replaces mapped -// every refusing reading to byte_size(0) and relied on the zero travelling through -// gunbc_runner_cores_per_slot and host_cpu_admitted_width until the placement called it -// ZeroRunnerWidth. That is the absorbing arm DESIGN forbids: it is a degradation wearing a -// refusal's name, and it was measured -- a missing unit conversion made the per-core figure 3 -// BYTES, which drove cores-per-slot to 5,726,623,062, the CPU axis to zero, and the whole fleet -// to deployment=refused cause=zero_runner_width, a diagnostic that named neither the ratio nor -// the conversion. The cause now travels with the refusal. -type RunnerMemoryPerCoreResolution - = MemoryPerCoreResolved { bytes: ByteSize } - | MemoryPerCoreUnresolved { cause: String } - -fn gunbc_runner_memory_per_core() -> RunnerMemoryPerCoreResolution { - runner_memory_per_core_of(reading: gunbc_runner_memory_per_core_reading()) -} - -// THE READING IS A PARAMETER SO THE REFUSAL HAS AN AUTHORABLE RED. A zero-argument derivation over -// the live census can only ever be exercised on whatever the survey currently says, so its refusing -// arms would be permanently unreachable and the check on them would be a decoration. Taking the -// reading lets a fixture hand this function each arm it claims to refuse. -fn runner_memory_per_core_of(reading: MemoryPerVcpuReading) -> RunnerMemoryPerCoreResolution { - match reading { - UniformMemoryPerVcpu { per_vcpu: q, sizes_agreeing: _ } => - match quoted_magnitude_bytes(quoted: q) { - ResolvedBytes { bytes: b } => MemoryPerCoreResolved { bytes: b } - QuotedBasisUnresolved { numeral: _, unit_label: l } => - MemoryPerCoreUnresolved { - cause: concat("the surveyed memory-per-vCPU ratio is uniform but carries no stated byte basis, so its numeral cannot be read as bytes without gunbc choosing a basis the vendor did not publish; unit label: ", l) - } - } - RatioNotUniformAcrossSizes { first_per_vcpu: _, divergent_label: dl, divergent_per_vcpu: _ } => - MemoryPerCoreUnresolved { - cause: concat("the surveyed provider's sizes do not agree on a memory-per-vCPU ratio, so there is no convention to adopt; first divergent size: ", dl) - } - NonIntegralRatioInSizes { label: l } => - MemoryPerCoreUnresolved { - cause: concat("a surveyed size divides memory by vCPUs non-integrally, so the provider states no whole ratio; size: ", l) - } - UnitLabelNotUniformAcrossSizes { first_label: fl, divergent_label: dl } => - MemoryPerCoreUnresolved { - cause: concat(concat("the surveyed sizes are quoted in more than one unit word, so their quotient is not a ratio: ", fl), concat(" against ", dl)) - } - NoShapesForProviderArchitecture => - MemoryPerCoreUnresolved { - cause: "the survey holds no shapes for the cited provider and architecture, so no ratio was read at all" - } - } -} +// CORES PER SLOT IS AN AUTHORED POLICY OF ONE, AND THE AUTHORITY IS AN OPERATOR RULING. +// +// Operator ruling, 2026-09-19, verbatim: "there is no CPU demand - the processes we run are single +// core - do NOT overthink it please". +// +// WHAT THIS REPLACES. cores_per_slot was ceil(memory_max / memory_per_core), where memory_per_core +// was read from a surveyed Ubicloud aarch64 catalog (3 GB decimal per vCPU) through +// gunbc.runner_shape_census. That figure is a vendor's commercial proportioning of an Ampere Altra +// Q80-30 and was answering a different question -- how a cloud prices memory against vCPUs -- while +// being consumed as how many cores one CI slot NEEDS, which is a fact about the workload. At the +// 26 GiB slot it answered 10 and bound the fleet at 128/10 = 12 per host. The workload runs single +// core, so the CPU axis was reserving ten times the demand from a price list. The survey reading, +// its per-arm refusal vocabulary (RunnerMemoryPerCoreResolution) and the derived resolution +// (RunnerCoresPerSlotResolution) are DELETED with it, not left beside this row: two answers to one +// question is the fork DESIGN 3 exists to prevent, and none of their refusing arms had a producer +// once the survey went. +// +// WHAT IT IS NOT. It is not derived from memory_max, not from a vendor catalog, and not a +// measurement: it is the placement policy the ruling states. It is also NOT a CPU ceiling -- +// gunbc_runner_slot_cpu_quota is SlotCpuQuotaUnbounded, so a slot may use every core the scheduler +// gives it under cpu_weight; this number only says how many slots the host's cited core count +// admits, and at 1 it admits all 128, which stops the CPU axis from binding. +data gunbc_runner_cores_per_slot_policy: HardwareThreadCount = hardware_thread_count(count: 1) -type RunnerCoresPerSlotResolution - = CoresPerSlotResolved { threads: HardwareThreadCount } - | CoresPerSlotUnresolved { cause: String } - -fn gunbc_runner_cores_per_slot() -> RunnerCoresPerSlotResolution { - match gunbc_runner_memory_per_core() { - MemoryPerCoreUnresolved { cause: c } => CoresPerSlotUnresolved { cause: c } - MemoryPerCoreResolved { bytes: per_core } => { - let want = byte_size_count(b: gunbc_runner_slot_memory_max) - let per = byte_size_count(b: per_core) - if per == 0 { - CoresPerSlotUnresolved { - cause: "the surveyed memory-per-vCPU ratio resolved to zero bytes, which would admit every core on the host against no memory at all" - } - } else { - CoresPerSlotResolved { threads: hardware_thread_count(count: (want + per - 1) / per) } - } - } - } +fn gunbc_runner_cores_per_slot() -> HardwareThreadCount { + gunbc_runner_cores_per_slot_policy } fn gunbc_runner_slice_cap_declared(host: HostIdentity) -> ByteSize { @@ -1183,36 +1043,6 @@ fn gunbc_runner_slot_allocation_refuses_effective_readback_reconciliation(obs: R // gunbc_runner_slot_oversubscription_within_slice survives as the general relation for callers // holding two independent numbers. -// THE FIRST CONJUNCT CONSUMES THE RESOLUTIONS AND REFUSES ON EITHER UNRESOLVED ARM. It previously -// passed gunbc_runner_cores_per_slot() to hardware_thread_count_value and gunbc_runner_memory_per_core() -// to byte_size_count after both had become resolution coproducts, and the corpus accepted it: a -// two-variant coproduct reached a parameter declared as a one-field measure at a CALL-argument -// position, and the required floor stayed green over 3,136 witnesses. It is recorded here rather -// than quietly repaired because it is a floor-level compiler admission -- DESIGN's "values inhabit -// declared types" baseline. -// -// THE SPECIMEN IS RECORDED, NOT YET ENROLLED, and the distinction is stated plainly because an -// annotation is not evidence (DESIGN 4c). test.claim.declared_type_inhabitance_direct_call_witness -// already refuses distinct unapplied constructors passed through a call, so this admission is NOT -// the general call-argument case and the discriminator is unmeasured: the candidates are a -// coproduct producer, an alias-expanded generic application at the parameter, and cross-module -// resolution. Asserting one of them here without the matrix would be the same guess that produced -// the unit bug above. The trigger is the capability: a probe matrix over producer kind x judgment -// position (return, call argument, record field, typed let) with one-axis REDs and matching -// positive controls, enrolled where the floor folds it -- after which this comment is redundant and -// goes. -fn runner_slot_core_memory_relation_holds(want: ByteSize) -> Bool { - match gunbc_runner_cores_per_slot() { - CoresPerSlotUnresolved { cause: _ } => false - CoresPerSlotResolved { threads: t } => - match gunbc_runner_memory_per_core() { - MemoryPerCoreUnresolved { cause: _ } => false - MemoryPerCoreResolved { bytes: per_core } => - hardware_thread_count_value(t: t) * byte_size_count(b: per_core) >= byte_size_count(b: want) - } - } -} - // A ZERO WIDTH IS ACCOUNTED, NOT FILTERED, AND THAT CHOICE IS THE POINT (2026-09-12). // // srv2 derives zero now that both of its bypass arms are deleted, and the obvious repair -- drop @@ -1231,11 +1061,22 @@ fn runner_slot_core_memory_relation_holds(want: ByteSize) -> Bool { // // THE FLEET RESULT IS THEREFORE INCOMPLETE AND SAYS SO, rather than being quietly reported as whole // over the hosts that happened to survive the filter. +// +// THE CORES-TIMES-MEMORY-PER-CORE CONJUNCT IS DELETED (2026-09-19), not rewritten: it asserted +// cores_per_slot * memory_per_core >= memory_max, which was the derivation of cores_per_slot read +// back as a relation, and the derivation is gone (gunbc_runner_cores_per_slot_policy). A compiler +// admission was recorded on that conjunct's earlier form and the record survives here: it once +// passed a two-variant resolution coproduct to a parameter declared as a one-field measure at a +// CALL-argument position and the corpus accepted it over 3,136 witnesses -- a floor-level "values +// inhabit declared types" admission. test.claim.declared_type_inhabitance_direct_call_witness +// refuses distinct unapplied constructors through a call, so this is not that case; the +// discriminator (coproduct producer, alias-expanded generic at the parameter, cross-module +// resolution) is unmeasured and the trigger is a probe matrix over producer kind x judgment +// position with one-axis REDs and positive controls, enrolled where the floor folds it. fn gunbc_runner_slot_allocation_wall_holds() -> Bool { let per_slot = gunbc_runner_slot_desired() let gibibyte_bytes = 1073741824 - runner_slot_core_memory_relation_holds(want: per_slot.memory_max) - && (byte_size_count(b: per_slot.memory_high) + gibibyte_bytes == byte_size_count(b: per_slot.memory_max)) + (byte_size_count(b: per_slot.memory_high) + gibibyte_bytes == byte_size_count(b: per_slot.memory_max)) && (byte_size_count(b: per_slot.memory_high) >= byte_size_count(b: gunbc_floor_minimum_viable_armed_budget)) && (byte_size_count(b: per_slot.memory_high) >= byte_size_count(b: gunbc_floor_measured_peak_observation)) && gunbc_floor_measured_peak_is_uncensored() diff --git a/dag/test/claim/executor_privileged_operation_witness_test.dag b/dag/test/claim/executor_privileged_operation_witness_test.dag index 9409272d770..cb1e0463ff3 100644 --- a/dag/test/claim/executor_privileged_operation_witness_test.dag +++ b/dag/test/claim/executor_privileged_operation_witness_test.dag @@ -24,10 +24,10 @@ import gunbc.runner_slot_allocation { gunbc_runner_slots_per_host, gunbc_runner_slot_cpu_quota, CpuQuotaResolved, - CpuQuotaRefused, + SlotCpuQuotaUnbounded, } -import gunbc.fabric_cell_effect { fabric_cell_slice_desired_directives, SliceDirectivesComplete, SliceDirectivesQuotaSurveyRefused } -import extdeps.systemd.unit_file { slice_cpu_quota, systemd_slice_directive_line } +import gunbc.fabric_cell_effect { fabric_cell_slice_desired_directives } +import extdeps.systemd.unit_file { slice_cpu_quota, slice_cpu_quota_unbounded, systemd_slice_directive_line } import gunbc.fleet_intent_network { operator_host_srv2, operator_host_srv4 } import gunbc.runner_host_deploy { admitted_deploy_for, RunnerHostDeploy } @@ -149,56 +149,48 @@ test fn witness_rendered_grants_carry_no_wildcard() -> Bool { // fabric_allocation_store_privileged_operations on a fabric host -- deriving that chain is // out of scope here. test fn witness_population_tracks_the_slot_roster() -> Bool { - match fabric_cell_slice_desired_directives() { - SliceDirectivesQuotaSurveyRefused { cause: _ } => false - SliceDirectivesComplete { directives: ds } => { - let slots = desired_runner_slot_members(deploy: wt_srv3_deploy()).length() - let activation = runner_slot_activation_operations(deploy: wt_srv3_deploy()).length() - let fabric_slots = fabric_cell_host_slots(deploy: wt_srv3_deploy()).length() - let directives = ds.length() - let fabric_expected = 1 + fabric_slots * (2 + directives + 1) - let allocation_store = fabric_allocation_store_privileged_operations(deploy: wt_srv3_deploy()).length() - let host_wide = runner_host_wide_privileged_operations(deploy: wt_srv3_deploy()).length() - runner_host_privileged_operations(deploy: wt_srv3_deploy()).length() - == runner_slot_teardown_grant_index_bound + (slots * 3) + host_wide + fabric_expected + allocation_store - && allocation_store == 2 - && fabric_cell_privileged_operations(deploy: wt_srv3_deploy()).length() == fabric_expected - && activation == slots * 2 - && slots > 0 - && fabric_slots > 0 - && host_wide > 0 - } - } + let ds = fabric_cell_slice_desired_directives() + let slots = desired_runner_slot_members(deploy: wt_srv3_deploy()).length() + let activation = runner_slot_activation_operations(deploy: wt_srv3_deploy()).length() + let fabric_slots = fabric_cell_host_slots(deploy: wt_srv3_deploy()).length() + let directives = ds.length() + let fabric_expected = 1 + fabric_slots * (2 + directives + 1) + let allocation_store = fabric_allocation_store_privileged_operations(deploy: wt_srv3_deploy()).length() + let host_wide = runner_host_wide_privileged_operations(deploy: wt_srv3_deploy()).length() + runner_host_privileged_operations(deploy: wt_srv3_deploy()).length() + == runner_slot_teardown_grant_index_bound + (slots * 3) + host_wide + fabric_expected + allocation_store + && allocation_store == 2 + && fabric_cell_privileged_operations(deploy: wt_srv3_deploy()).length() == fabric_expected + && activation == slots * 2 + && slots > 0 + && fabric_slots > 0 + && host_wide > 0 } -// THE CPUQuota GRANT IS THE SURVEYED DIRECTIVE, NOT A TRANSCRIBED PERCENTAGE. The generated -// sudoers body is `artifact_generate` of this host's row; the assignment word is -// `systemd_slice_directive_line(slice_cpu_quota(threads))` for the same -// `gunbc_runner_slot_cpu_quota` the effect emits. RED: hand-insert `CPUQuota=600%` into the -// committed file without the survey resolving, or resolve to a different thread count than the -// committed projection, and either this or the generated-artifact gate goes false. A refused -// survey must leave no `CPUQuota=` grant, because there is then no argv to authorize. -test fn witness_fabric_slice_cpu_quota_grant_is_the_surveyed_directive() -> Bool { +// THE CPUQuota GRANT IS THE DECLARED DIRECTIVE, NOT A TRANSCRIBED PERCENTAGE. The generated +// sudoers body is `artifact_generate` of this host's row; the assignment word is the +// `systemd_slice_directive_line` of the directive `gunbc_runner_slot_cpu_quota` selects -- a +// thread ceiling under CpuQuotaResolved, the empty assignment `CPUQuota=` under +// SlotCpuQuotaUnbounded (the 2026-09-19 policy). RED: hand-insert `CPUQuota=600%` into the +// committed file, or declare a different quota than the committed projection, and either this +// or the generated-artifact gate goes false. The unbounded arm asserts the OTHER direction too: no +// percentage grant may survive in the file, because a 100% quota at one core per slot is the +// throttle the ruling forbids. +test fn witness_fabric_slice_cpu_quota_grant_is_the_declared_directive() -> Bool { + let ds = fabric_cell_slice_desired_directives() + let desired_lines = join(map(ds, d => systemd_slice_directive_line(directive: d)), "\n") match gunbc_runner_slot_cpu_quota() { - CpuQuotaResolved { threads: t } => - match fabric_cell_slice_desired_directives() { - SliceDirectivesQuotaSurveyRefused { cause: _ } => false - SliceDirectivesComplete { directives: ds } => { - let assignment = systemd_slice_directive_line(directive: slice_cpu_quota(threads: t)) - let desired_lines = join( - map(ds, d => systemd_slice_directive_line(directive: d)), - "\n", - ) - string_contains(s: srv3_runner_host_sudoers(), pattern: assignment) - && string_contains(s: desired_lines, pattern: assignment) - } - } - CpuQuotaRefused { cause: _ } => - match fabric_cell_slice_desired_directives() { - SliceDirectivesComplete { directives: _ } => false - SliceDirectivesQuotaSurveyRefused { cause: _ } => - !string_contains(s: srv3_runner_host_sudoers(), pattern: "CPUQuota=") - } + CpuQuotaResolved { threads: t } => { + let assignment = systemd_slice_directive_line(directive: slice_cpu_quota(threads: t)) + string_contains(s: srv3_runner_host_sudoers(), pattern: assignment) + && string_contains(s: desired_lines, pattern: assignment) + } + SlotCpuQuotaUnbounded => { + let assignment = systemd_slice_directive_line(directive: slice_cpu_quota_unbounded()) + string_contains(s: srv3_runner_host_sudoers(), pattern: join([assignment, "\n"], "")) + && string_contains(s: desired_lines, pattern: assignment) + && !string_contains(s: srv3_runner_host_sudoers(), pattern: "%") + } } } diff --git a/dag/test/claim/fabric/fabric_cell_converge_witness_test.dag b/dag/test/claim/fabric/fabric_cell_converge_witness_test.dag index f56f2c8dcc7..c8801c1eaf4 100644 --- a/dag/test/claim/fabric/fabric_cell_converge_witness_test.dag +++ b/dag/test/claim/fabric/fabric_cell_converge_witness_test.dag @@ -5,7 +5,9 @@ import std.types { list_length, String, NonEmptyStr, Int } import gunbc.build_cache_instance { RunnerSlotIdentity, runner_slot_instance_name } import gunbc.fleet_intent_network { operator_host_srv3, operator_host_srv4 } import gunbc.ownership { Owned } -import gunbc.runner_slot_allocation { gunbc_runner_slot_desired, CpuQuotaRefused } +import gunbc.runner_slot_allocation { gunbc_runner_slot_desired } +import extdeps.systemd { parse_systemd_cpu_quota_per_sec_show, systemd_cpu_quota_per_sec_identity } +import v2.std.optional { Present, Absent } import std.measure { byte_size_count } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import gunbc.fabric_cell_observation_admission { @@ -43,7 +45,6 @@ import gunbc.fabric_cell_converge { fabric_cell_boundary_address_digest_of, fabric_cell_slice_name, fabric_cell_address_standing, - fabric_cell_address_standing_for_quota, FabricCellAddressRef, fabric_cell_family_membership, FabricCellFamilyMembership, @@ -633,28 +634,22 @@ test fn witness_drifted_quota_standing_is_not_converged() -> Bool { ) } -test fn witness_a_refused_cpu_quota_survey_blocks_standing_on_an_absent_boundary() -> Bool { - match fabric_cell_address_standing_for_quota( - slot: srv3_fabric_slot(), - d: AddressMissingOnHost { - ref: FabricCellAddressRef { - slot: srv3_fabric_slot(), - address: FabricCellResourceBoundaryAddress, - }, - }, - quota: CpuQuotaRefused { cause: "fixture: cores-per-slot unresolved" }, - ) { - StandingBlocked { disposition: d } => - match d { - AddressUnreadable { ref: _, cause: c } => - string_contains(s: c, pattern: "desired CPU quota refused: fixture: cores-per-slot unresolved") - AddressPresent { ref: _, member: _ } => false - AddressMissingOnHost { ref: _ } => false - AddressUnobserved { ref: _ } => false - AddressContradicted { ref: _ } => false - } - StandingSatisfied { ref: _ } => false - StandingObservedDrift { ref: _, observed_state_digest: _ } => false - StandingExplicitlyAbsent { ref: _ } => false +// A HOST THAT PRINTS `infinity` IS SATISFIED UNDER THE UNBOUNDED ROW, THROUGH THE PARSER. The +// observed side never sees the desired row: it reads CPUQuotaPerSecUSec, parses it, and supplies +// the identity, so this drives the observed value through parse_systemd_cpu_quota_per_sec_show +// exactly as fabric_cell_boundary_address_probe does. RED: a desired digest that spelled unbounded +// as a literal beside the parser's ("" or "unbounded") reads every live cell as drifted; the drift +// test above is the other half -- a host that reports a one-thread quota against the same row is +// not converged, which is the throttle the 2026-09-19 ruling forbids showing up as drift-to-repair. +test fn witness_an_unbounded_host_quota_is_satisfied_under_the_unbounded_row() -> Bool { + match systemd_cpu_quota_per_sec_identity(q: parse_systemd_cpu_quota_per_sec_show(raw: "infinity")) { + Absent => false + Present { value: observed } => + standing_is_satisfied( + st: fabric_cell_address_standing( + slot: srv3_fabric_slot(), + d: boundary_address_present(member: quota_boundary_member(quota_identity: observed)), + ), + ) } } diff --git a/dag/test/claim/fabric/fabric_cell_effect_witness_test.dag b/dag/test/claim/fabric/fabric_cell_effect_witness_test.dag index 111cabe0225..7e34dbd8f3b 100644 --- a/dag/test/claim/fabric/fabric_cell_effect_witness_test.dag +++ b/dag/test/claim/fabric/fabric_cell_effect_witness_test.dag @@ -20,7 +20,9 @@ import gunbc.fabric_cell_converge { fabric_cell_base_dir, fabric_cell_slice_name, } -import gunbc.runner_slot_allocation { CpuQuotaRefused } +import gunbc.runner_slot_allocation { RunnerSlotCpuQuota, CpuQuotaResolved, SlotCpuQuotaUnbounded } +import std.measure { hardware_thread_count } +import extdeps.systemd.unit_file { SystemdSliceDirective, systemd_slice_directive_line } import gunbc.fabric_cell_effect { FabricCellRealization, CellPlanRealizable, @@ -34,7 +36,6 @@ import gunbc.fabric_cell_effect { CellChangeNotYetRealizable, CellTeardownNotYetRealizable, CellRosterDuplicateKey, - CellCpuQuotaSurveyRefused, fabric_cell_realization, fabric_cell_realization_refusal_count, fabric_cell_plan_effects, @@ -219,7 +220,6 @@ fn is_teardown_unrealizable(r: FabricCellRealization) -> Bool { CellTeardownRefusedByOwnership { address_wire: _, cause: _ } => false CellRosterDuplicateKey { address_wire: _ } => false CellChangeNotYetRealizable { address_wire: _ } => false - CellCpuQuotaSurveyRefused { address_wire: _, cause: _ } => false })) CellPlanRealizable { effects: _ } => false } @@ -233,7 +233,6 @@ fn is_ownership_refusal(r: FabricCellRealization) -> Bool { CellRosterDuplicateKey { address_wire: _ } => false CellChangeNotYetRealizable { address_wire: _ } => false CellTeardownNotYetRealizable { address_wire: _ } => false - CellCpuQuotaSurveyRefused { address_wire: _, cause: _ } => false })) CellPlanRealizable { effects: _ } => false } @@ -247,26 +246,40 @@ fn is_change_unrealizable(r: FabricCellRealization) -> Bool { CellTeardownRefusedByOwnership { address_wire: _, cause: _ } => false CellRosterDuplicateKey { address_wire: _ } => false CellTeardownNotYetRealizable { address_wire: _ } => false - CellCpuQuotaSurveyRefused { address_wire: _, cause: _ } => false })) CellPlanRealizable { effects: _ } => false } } -test fn witness_a_refused_cpu_quota_survey_refuses_boundary_add_rather_than_emitting_five_directives() -> Bool { - match fabric_cell_boundary_add_realization( - slot: srv3_fabric_slot(), - quota: CpuQuotaRefused { cause: "fixture: cores-per-slot unresolved" }, - ) { - AddressRealizable { effect: _ } => false - AddressRefused { refusal: r } => - match r { - CellCpuQuotaSurveyRefused { address_wire: _, cause: c } => - c == "fixture: cores-per-slot unresolved" - CellTeardownRefusedByOwnership { address_wire: _, cause: _ } => false - CellRosterDuplicateKey { address_wire: _ } => false - CellChangeNotYetRealizable { address_wire: _ } => false - CellTeardownNotYetRealizable { address_wire: _ } => false +// THE BOUNDARY ADD RENDERS SIX DIRECTIVES UNDER EITHER QUOTA ARM, AND THE SIXTH IS THE ONE THAT +// DIFFERS. Under a resolved quota the CPUQuota line carries the thread ceiling; under the unbounded +// policy it is the empty assignment `CPUQuota=`, an affirmative unset. The RED on the unbounded +// side is the widen this used to refuse: an implementation that dropped the line and emitted five +// directives would leave a once-quota'd cell throttled while the desired digest says infinity. +// Both arms are driven by fixture so neither is permanently green on the production row. +test fn witness_boundary_add_renders_the_quota_line_under_both_arms() -> Bool { + let bounded = boundary_add_directive_lines(quota: CpuQuotaResolved { threads: hardware_thread_count(count: 6) }) + let unbounded = boundary_add_directive_lines(quota: SlotCpuQuotaUnbounded) + list_length(items: bounded) == 6 + && list_length(items: unbounded) == 6 + && has_line(lines: bounded, line: "CPUQuota=600%") + && has_line(lines: unbounded, line: "CPUQuota=") + && !has_line(lines: unbounded, line: "CPUQuota=600%") + && !has_line(lines: unbounded, line: "CPUQuota=100%") +} + +fn has_line(lines: List, line: String) -> Bool { + fold(lines, init: false, f: (acc, l) => acc || l == line) +} + +fn boundary_add_directive_lines(quota: RunnerSlotCpuQuota) -> List { + match fabric_cell_boundary_add_realization(slot: srv3_fabric_slot(), quota: quota) { + AddressRefused { refusal: _ } => [] as List + AddressRealizable { effect: e } => + match e { + FabricCellSliceEnsure { slice_unit: _, directives: ds } => + map(ds, d => systemd_slice_directive_line(directive: d)) + FabricCellDirectoryEnsure { path: _, owner: _ } => [] as List } } } diff --git a/dag/test/claim/host/host_allocation_conservation_test.dag b/dag/test/claim/host/host_allocation_conservation_test.dag index 47d075228e0..1a63e4e1758 100644 --- a/dag/test/claim/host/host_allocation_conservation_test.dag +++ b/dag/test/claim/host/host_allocation_conservation_test.dag @@ -7,7 +7,8 @@ module test.claim.host_allocation_conservation // already using; making them explicit costs nothing and buys a witness you can execute while you // are writing it, which is the difference between finding a defect now and finding it in CI. import std.types { String, Bool, Int, List } -import std.measure { ByteSize, byte_size } +import std.measure { ByteSize, byte_size, hardware_thread_count_value } +import extdeps.cpu.ampere { altra_max_m12830_catalog } import product.placement_supply { HostIdentity } import gunbc.fleet_intent_network { operator_host_srv1, operator_host_srv2, operator_host_srv3, operator_host_srv4, @@ -46,7 +47,9 @@ import gunbc.runner_slot_allocation { RunnerSlotShapeRefusedZeroMemoryMax, RunnerSlotDesired, RunnerSlotLabels, - CpuQuotaRefused, + SlotCpuQuotaUnbounded, + gunbc_runner_cores_per_slot, + host_cpu_admitted_width, host_disk_admitted_width, DiskWidthUnconstrained, DiskAdmitsWidth, @@ -964,7 +967,7 @@ fn zero_memory_slot_shape() -> RunnerSlotDesired { memory_high: byte_size(0), tasks_max: 16384, cpu_weight: 100, - cpu_quota: CpuQuotaRefused { cause: "zero-memory fixture carries no surveyed quota" }, + cpu_quota: SlotCpuQuotaUnbounded, labels: RunnerSlotLabels { github_labels: ["gunbc-heavy"] }, } } @@ -984,58 +987,20 @@ test fn w_live_slot_shape_is_admitted() -> Bool { } } -// THE RATIO'S REFUSAL, RED AND POSITIVE CONTROL. The shape this replaces mapped every refusing -// memory-per-vCPU reading to byte_size(0) and let the zero travel: through cores-per-slot, through -// the CPU axis, into a fleet-wide deployment=refused cause=zero_runner_width that named neither the -// ratio nor the conversion that broke it. That is an absorbing arm, not a refusal, and it hid a -// real defect for a full CI cycle -- the reading is quoted in the vendor's own unit and was being -// read as a byte count, making the per-core figure 3 BYTES. +// THE CPU AXIS IS A QUOTIENT OF TWO CITED CONSTANTS AND IT DOES NOT BIND. cores_per_slot is the +// authored policy of one (gunbc_runner_cores_per_slot_policy, operator ruling 2026-09-19); the host +// core count is the Ampere catalog row. The rows that stood here -- a basisless-ratio refusal, an +// absent-survey refusal, and w_live_ratio_resolves_to_the_vendors_decimal_gigabytes -- were the +// red and positive control of a vendor memory-per-vCPU reading that no longer exists; their +// subject was deleted, so they are deleted rather than retargeted at a literal. // -// Each refusing arm is now handed to the derivation directly, which is only possible because -// runner_memory_per_core_of takes the reading as a parameter. -fn basisless_uniform_reading() -> MemoryPerVcpuReading { - UniformMemoryPerVcpu { - per_vcpu: QuotedWithoutBasis { numeral: 3, unit_label: "GB" }, - sizes_agreeing: 5, - } -} - -test fn w_basisless_ratio_refuses_rather_than_answering_zero() -> Bool { - match runner_memory_per_core_of(reading: basisless_uniform_reading()) { - MemoryPerCoreUnresolved { cause: _ } => true - MemoryPerCoreResolved { bytes: _ } => false - } -} - -test fn w_absent_survey_refuses_rather_than_answering_zero() -> Bool { - match runner_memory_per_core_of(reading: NoShapesForProviderArchitecture) { - MemoryPerCoreUnresolved { cause: _ } => true - MemoryPerCoreResolved { bytes: _ } => false - } -} - -// THE POSITIVE CONTROL, and it is also the unit-conversion regression control: the live survey is -// ubicloud aarch64, whose catalog states decimal gigabytes, so the resolved figure must be the -// vendor's 3 GB in bytes. Reading that numeral as gibibytes, or failing to convert it at all, -// both go red here. -test fn w_live_ratio_resolves_to_the_vendors_decimal_gigabytes() -> Bool { - match gunbc_runner_memory_per_core() { - MemoryPerCoreResolved { bytes: b } => byte_size_count(b: b) == 3000000000 - MemoryPerCoreUnresolved { cause: _ } => false - } -} - -// AND THE SLOT WIDTH THE RATIO IMPLIES, asserted as the derivation rather than as its current -// answer: 128 cores over a per-slot core count that is itself derived from the slot's memory -// ceiling and the surveyed ratio. -test fn w_cpu_axis_admits_a_width_from_the_resolved_ratio() -> Bool { - match gunbc_runner_cores_per_slot() { - CoresPerSlotUnresolved { cause: _ } => false - CoresPerSlotResolved { threads: t } => - hardware_thread_count_value(t: t) > 0 - && (match host_cpu_admitted_width(host: operator_host_srv1) { - CpuAdmitsWidth { slots: n } => n == altra_max_m12830_catalog.cores / hardware_thread_count_value(t: t) - CpuWidthUnresolved { cause: _ } => false - }) - } +// RED: a policy re-derived from memory_max resolves above 1 at the 26 GiB slot and fails the first +// clause; an axis that stopped consulting the catalog (a 128 written here) fails the second the day +// the catalog row moves; and the third clause is the ruling's whole consequence -- with the CPU +// axis at 128 the memory admission IS the committed width on every derived host. +test fn w_cpu_axis_admits_the_catalog_core_count_at_one_core_per_slot() -> Bool { + hardware_thread_count_value(t: gunbc_runner_cores_per_slot()) == 1 + && host_cpu_admitted_width(host: operator_host_srv1) == altra_max_m12830_catalog.cores + && gunbc_runner_slots_per_host(host: operator_host_srv1) == host_memory_admitted_width(host: operator_host_srv1) + && gunbc_runner_slots_per_host(host: operator_host_srv3) == host_memory_admitted_width(host: operator_host_srv3) } diff --git a/dag/test/claim/runner/runner_microvm_witness_test.dag b/dag/test/claim/runner/runner_microvm_witness_test.dag index b8bd524067c..3cf54f25649 100644 --- a/dag/test/claim/runner/runner_microvm_witness_test.dag +++ b/dag/test/claim/runner/runner_microvm_witness_test.dag @@ -69,11 +69,9 @@ import extdeps.github.actions_runner { actions_runner_release_2_337_0 } import std.measure { hardware_thread_count_value, hardware_thread_count, byte_size, gibibyte_to_byte_size } import gunbc.runner_slot_allocation { gunbc_runner_cores_per_slot, - CoresPerSlotResolved, - CoresPerSlotUnresolved, gunbc_runner_slot_desired, CpuQuotaResolved, - CpuQuotaRefused, + SlotCpuQuotaUnbounded, } @@ -595,21 +593,25 @@ test fn the_production_reserve_is_the_declared_allowance() -> Bool { // // It judges MODEL admission -- whether such a row is one this repository would accept -- and boots // nothing; no Firecracker process is involved at any step. -// PRODUCTION SIZING WITHDRAWS THE DECLARED CONFIGURATION NOW THAT THE CELL GRANTS AN ABSOLUTE -// ENTITLEMENT. This previously asserted that the fleet's declared VM reached admission. That wall -// landed: Absolute is inhabited, so gunbc_runner_microvm_vm_config is Unresolved and names the -// repair. Per DESIGN 4b(4) the expecting-green probe flips rather than retiring. -test fn the_production_vm_config_refuses_while_topology_is_still_the_stale_declaration() -> Bool { +// PRODUCTION SIZING SERVES THE DECLARED CONFIGURATION AGAIN, BECAUSE THE CELL GRANTS NO ABSOLUTE +// ENTITLEMENT. This row has flipped twice and both flips are recorded: it first asserted that the +// fleet's declared VM reached admission; then the slot survey resolved, Absolute was inhabited, and +// the row asserted the withdrawal (Unresolved naming the repair). As of 2026-09-19 the slot row +// declares no CPU ceiling by operator ruling, the entitlement is RelativeOnly on the production +// path, and the declaration is the honest answer once more -- so the row asserts that the fleet's +// config RESOLVES to the declared size. The RED is the coupling coming back: a cpu_quota re-derived +// from cores_per_slot would inhabit Absolute and withdraw the declaration here. +test fn the_production_vm_config_resolves_to_the_declared_size_under_no_ceiling() -> Bool { match gunbc_runner_microvm_vm_config( rootfs_path: "/var/lib/gunbc/cell/rootfs.ext4", jit_config_path: "/var/lib/gunbc/cell/jitcfg.ext4", kernel_path: "/var/lib/gunbc/cell/vmlinux", net: GuestNetworkAbsent { cause: "this control judges admission, not the per-slot tap" }, ) { - MicroVmConfigResolved { config: _ } => false - MicroVmConfigUnresolved { cause: c } => - cause_mentions(cause: c, needle: "derivable") - && cause_mentions(cause: c, needle: "stale") + MicroVmConfigUnresolved { cause: _ } => false + MicroVmConfigResolved { config: c } => + c.machine_config.vcpu_count + == hardware_thread_count_value(t: gunbc_runner_microvm_declared_size.vcpu_count) } } @@ -625,18 +627,23 @@ test fn the_declared_guest_plus_allowance_sits_under_the_cell_throttle() -> Bool guest + allowance <= byte_size_count(b: gunbc_runner_slot_desired().memory_high) } -// THE CELL GRANTS AN ABSOLUTE CPU ENTITLEMENT WHEN THE SLOT SURVEY RESOLVES, AND THAT IS THE SAME -// THREAD COUNT AS gunbc_runner_slot_desired().cpu_quota. If this ever falls back to RelativeOnly -// while the survey is Resolved, the slice quota and the guest-sizing entitlement have forked. -test fn the_cell_grants_the_surveyed_absolute_cpu_entitlement() -> Bool { +// THE CELL'S ENTITLEMENT IS THE SLOT ROW'S QUOTA SEEN FROM THE GUEST, ARM FOR ARM. A resolved +// quota is an Absolute entitlement of the same thread count; the unbounded quota (the 2026-09-19 +// policy, and the production row) is RelativeOnly with a cause that names that policy. If either +// arm ever maps to the other, the slice quota and the guest-sizing entitlement have forked. +test fn the_cell_entitlement_is_the_slot_quota_arm_for_arm() -> Bool { match gunbc_runner_slot_desired().cpu_quota { - CpuQuotaRefused { cause: _ } => false CpuQuotaResolved { threads: want } => match gunbc_runner_microvm_cell_cpu_entitlement() { CpuEntitlementRelativeOnly { cause: _ } => false CpuEntitlementAbsolute { threads: got } => hardware_thread_count_value(t: want) == hardware_thread_count_value(t: got) } + SlotCpuQuotaUnbounded => + match gunbc_runner_microvm_cell_cpu_entitlement() { + CpuEntitlementAbsolute { threads: _ } => false + CpuEntitlementRelativeOnly { cause: c } => cause_mentions(cause: c, needle: "2026-09-19") + } } } @@ -723,11 +730,9 @@ test fn a_guest_without_a_tap_carries_no_interface_and_one_with_a_tap_does() -> && count(mvp_sized_config().network_interfaces) == 1 } -// THE CORE COUNT RESOLVES AND THE CELL NOW GRANTS THAT COUNT AS AN ABSOLUTE ENTITLEMENT. This -// witness keeps the survey fact true so a sizing refusal cannot be misread as "the survey failed". -test fn the_cell_core_count_still_resolves() -> Bool { - match gunbc_runner_cores_per_slot() { - CoresPerSlotUnresolved { cause: _ } => false - CoresPerSlotResolved { threads: t } => hardware_thread_count_value(t: t) > 0 - } +// THE CORE COUNT IS A POLICY AND IT IS POSITIVE. cores_per_slot no longer resolves or refuses -- +// it is authored (gunbc_runner_cores_per_slot_policy) -- so the only thing left to keep true is +// that the policy is not zero, which would admit every core against no slot at all. +test fn the_cell_core_count_is_a_positive_policy() -> Bool { + hardware_thread_count_value(t: gunbc_runner_cores_per_slot()) > 0 } diff --git a/dag/test/claim/runner/runner_placement_witness_test.dag b/dag/test/claim/runner/runner_placement_witness_test.dag index c2d4a1e6dd7..e1ad32c84fd 100644 --- a/dag/test/claim/runner/runner_placement_witness_test.dag +++ b/dag/test/claim/runner/runner_placement_witness_test.dag @@ -29,10 +29,7 @@ test fn witness_live_plan_is_conservation_sound() -> Bool { && all(hosts, h => h.runner_count == gunbc_runner_slots_per_host(host: h.identity) && h.runner_count <= host_memory_admitted_width(host: h.identity) - && (match host_cpu_admitted_width(host: h.identity) { - CpuAdmitsWidth { slots: c } => h.runner_count <= c - CpuWidthUnresolved { cause: _ } => false - }) + && h.runner_count <= host_cpu_admitted_width(host: h.identity) && byte_size_count(b: h.per_runner_memory_cap) == byte_size_count(b: gunbc_ci_runner_cgroup_memory_cap) && byte_size_count(b: h.runner_slice_cap) == byte_size_count(b: gunbc_runner_pool_budget_for(host: h.identity)) && hardware_thread_count_value(t: h.build_tokens) > 0 @@ -284,7 +281,6 @@ test fn a_refused_host_does_not_erase_a_derived_one() -> Bool { ZeroRunnerWidth => false JobserverCitationAbsent { cause: _ } => false HostCpuAllocationUnchargeable { cause: _ } => false - RunnerCoreRatioUnresolved { cause: _ } => false HostNotEnrolledInFleet => false } HostRunnerDeploymentDerived { deployment: _ } => false diff --git a/dag/test/claim/runner/runner_slot_allocation_witness_test.dag b/dag/test/claim/runner/runner_slot_allocation_witness_test.dag index 7570eaaf652..d2d2bebdb71 100644 --- a/dag/test/claim/runner/runner_slot_allocation_witness_test.dag +++ b/dag/test/claim/runner/runner_slot_allocation_witness_test.dag @@ -15,10 +15,9 @@ import gunbc.runner_slot_allocation { gunbc_runner_slot_oversubscription_within_slice, gunbc_runner_slot_desired, gunbc_runner_cores_per_slot, - CoresPerSlotResolved, - CoresPerSlotUnresolved, + host_cpu_admitted_width, CpuQuotaResolved, - CpuQuotaRefused, + SlotCpuQuotaUnbounded, gunbc_runner_slots_per_host, host_memory_admitted_width, host_memory_width_is_established, @@ -138,8 +137,7 @@ test fn no_host_bypasses_derivation_with_a_held_width() -> Bool { // runner_slot_allocation_committed_widths_MATCH_MEMORY_ADMISSION was an accurate description of a // coincidence -- memory was the binding axis on every host, so committed equalled admitted -- and // a name that states a coincidence as a law is how the next reader learns the wrong invariant. -// Committed width is the MINIMUM over memory, CPU and disk wherever it is DERIVED, and on the three -// hosts that derive it, it is now strictly below what memory admits. +// Committed width is the MINIMUM over memory, CPU and disk wherever it is DERIVED. // // srv2 IS DELIBERATELY NOT HELD TO THAT LAW, AND THE CONJUNCT ASSERTING IT WAS DELETED RATHER THAN // RETUNED. It read `committed == host_memory_admitted_width(srv2)`, which related srv2's width to a @@ -148,29 +146,29 @@ test fn no_host_bypasses_derivation_with_a_held_width() -> Bool { // property under test. An assertion whose oracle is an unestablished derivation is worse than a // missing one, because it reads as coverage. srv2's width is asserted on its own terms, as a hold, // by srv2_width_is_held_rather_than_derived above. -// CPU STILL BINDS AT 26 GiB, AND THE REASON IS THAT THE TWO AXES ARE NOT INDEPENDENT. // -// The first draft of this row asserted that the binding axis flipped to memory -- srv1 15, -// srv3/srv4 17 -- and it was WRONG, caught by this witness rather than by reading. cores_per_slot -// is DERIVED from the slot's memory ceiling (`gunbc_runner_cores_per_slot`: -// ceil(memory_max / memory_per_core), against the cited Ubicloud arm64 ratio of 3 GB decimal per -// vCPU). So raising memory 16 -> 26 GiB raises cores_per_slot 6 -> 10, and the CPU axis falls -// 128/6 = 21 to 128/10 = 12. Memory admits 15 and 17; CPU admits 12; the minimum is 12 everywhere. -// -// THE COUPLING IS THE FINDING AND IT IS RECORDED HERE BECAUSE IT IS INVISIBLE AT THE LITERAL. A -// reader raising the memory ceiling sees a memory decision and pays a CPU one, at a ratio taken -// from a cloud vendor's shape catalog rather than from any measurement of this workload -- whose -// floor run uses about one core sustained (cpu 20 min against 24 min wall). Most of the width lost -// by this change is therefore not bought by the memory it needed; it is bought by CPU quota -// nothing asked for. +// MEMORY BINDS AGAIN AS OF 2026-09-19, AND THIS TIME BY POLICY RATHER THAN COINCIDENCE. From +// 2026-08-23 to 2026-09-19 cores_per_slot was DERIVED from the slot's memory ceiling against a +// vendor memory-per-vCPU ratio, so raising memory 16 -> 26 GiB took cores_per_slot 6 -> 10 and the +// CPU axis 21 -> 12, binding every host at 12 -- a coupling this witness caught when a draft +// claimed 15/17/17. The operator ruling of 2026-09-19 ("there is no CPU demand - the processes we +// run are single core") replaced the derivation with an authored policy of 1 +// (gunbc_runner_cores_per_slot_policy): the CPU axis admits 128 on every M128-30 host and cannot +// bind below memory on any host in this fleet, so the committed widths are the memory admissions +// 15 and 17. The equality conjuncts below are the RED for the coupling coming back: a +// cores_per_slot re-derived from memory_max would drop these widths below memory admission and +// fail the `== host_memory_admitted_width` clauses, and the CPU clause pins the axis at the +// cited core count so a policy above 1 is a visible edit here rather than a silent narrowing. test fn runner_slot_allocation_committed_widths_are_the_minimum_over_axes() -> Bool { - gunbc_runner_slots_per_host(host: operator_host_srv1) == 12 + gunbc_runner_slots_per_host(host: operator_host_srv1) == 15 && gunbc_runner_slots_per_host(host: operator_host_srv2) == 0 - && gunbc_runner_slots_per_host(host: operator_host_srv3) == 12 - && gunbc_runner_slots_per_host(host: operator_host_srv4) == 12 - && gunbc_runner_slots_per_host(host: operator_host_srv1) < host_memory_admitted_width(host: operator_host_srv1) - && gunbc_runner_slots_per_host(host: operator_host_srv3) < host_memory_admitted_width(host: operator_host_srv3) - && gunbc_runner_slots_per_host(host: operator_host_srv4) < host_memory_admitted_width(host: operator_host_srv4) + && gunbc_runner_slots_per_host(host: operator_host_srv3) == 17 + && gunbc_runner_slots_per_host(host: operator_host_srv4) == 17 + && gunbc_runner_slots_per_host(host: operator_host_srv1) == host_memory_admitted_width(host: operator_host_srv1) + && gunbc_runner_slots_per_host(host: operator_host_srv3) == host_memory_admitted_width(host: operator_host_srv3) + && gunbc_runner_slots_per_host(host: operator_host_srv4) == host_memory_admitted_width(host: operator_host_srv4) + && host_cpu_admitted_width(host: operator_host_srv1) == 128 + && host_cpu_admitted_width(host: operator_host_srv3) > host_memory_admitted_width(host: operator_host_srv3) } test fn runner_slot_allocation_srv4_disk_unconstrained_at_allocation() -> Bool { @@ -433,14 +431,15 @@ test fn introducing_the_fabric_slot_deregisters_no_live_runner() -> Bool { == gunbc_runner_slots_per_host(host: operator_host_srv3) - 1 } -test fn the_slot_row_quota_is_the_surveyed_cores_per_slot() -> Bool { - match gunbc_runner_cores_per_slot() { - CoresPerSlotUnresolved { cause: _ } => false - CoresPerSlotResolved { threads: t } => - match gunbc_runner_slot_desired().cpu_quota { - CpuQuotaRefused { cause: _ } => false - CpuQuotaResolved { threads: q } => - hardware_thread_count_value(t: t) == hardware_thread_count_value(t: q) - } +// THE SLOT ROW DECLARES ONE CORE PER SLOT AND NO CPU CEILING, AND THE TWO ARE NOT ONE FACT. Until +// 2026-09-19 the quota WAS the cores_per_slot, so this row asserted their equality; under the +// ruling a 100% quota at one core per slot would throttle parallel link/codegen, so the quota is +// SlotCpuQuotaUnbounded while cores_per_slot is 1. RED: a quota re-derived from cores_per_slot +// resolves to one thread and fails the first clause; a cores_per_slot policy above 1 fails the +// second. +test fn the_slot_row_declares_one_core_and_no_ceiling() -> Bool { + match gunbc_runner_slot_desired().cpu_quota { + CpuQuotaResolved { threads: _ } => false + SlotCpuQuotaUnbounded => hardware_thread_count_value(t: gunbc_runner_cores_per_slot()) == 1 } } diff --git a/dag/test/claim/systemd_unit_file_witness_test.dag b/dag/test/claim/systemd_unit_file_witness_test.dag index 7f992304f1e..cb1772d8c2e 100644 --- a/dag/test/claim/systemd_unit_file_witness_test.dag +++ b/dag/test/claim/systemd_unit_file_witness_test.dag @@ -1,7 +1,7 @@ module test.claim.systemd_unit_file_witness_test import std.types { Bool, String, NonEmptyStr } -import std.measure { ByteSize, byte_size } +import std.measure { ByteSize, byte_size, hardware_thread_count } import extdeps.systemd.unit_file { SystemdUnitFile, ServiceUnitFile, @@ -50,6 +50,8 @@ import extdeps.systemd.unit_file { slice_tasks_max, slice_cpu_weight, slice_memory_max_unlimited, + slice_cpu_quota, + slice_cpu_quota_unbounded, slice_memory_high_unlimited, slice_memory_swap_max_unlimited, slice_tasks_max_unlimited, @@ -371,6 +373,16 @@ test fn an_unlimited_ceiling_is_not_a_zero_ceiling() -> Bool { != systemd_slice_directive_line(directive: slice_memory_max(bytes: byte_size(count: 0))) } +// `CPUQuota=` HAS NO `infinity` SPELLING, SO ITS UNSET IS THE EMPTY ASSIGNMENT. The RED is a mint +// that rendered the reset through the unlimited sentinel (`CPUQuota=infinity`, which systemd's +// parser rejects) or through the thread arm at zero (`CPUQuota=0%`, the strictest ceiling there +// is). Both are the same defect the memory test above guards, on the one knob whose wire differs. +test fn the_cpu_quota_unset_renders_the_empty_assignment() -> Bool { + systemd_slice_directive_line(directive: slice_cpu_quota_unbounded()) == "CPUQuota=" + && systemd_slice_directive_line(directive: slice_cpu_quota_unbounded()) + != systemd_slice_directive_line(directive: slice_cpu_quota(threads: hardware_thread_count(count: 0))) +} + fn slice_sample() -> SystemdUnitFile { SliceUnitFile { unit: [ diff --git a/provisioning/srv1/gunbc-ghrunner.sudoers b/provisioning/srv1/gunbc-ghrunner.sudoers index 0754fae9551..468512ec291 100644 --- a/provisioning/srv1/gunbc-ghrunner.sudoers +++ b/provisioning/srv1/gunbc-ghrunner.sudoers @@ -75,6 +75,9 @@ ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-10 ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-11 ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-12 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-13 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-14 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv1-15 ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv1-01.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv1-01.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv1-02.service @@ -99,6 +102,12 @@ ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv1-11.s ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv1-11.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv1-12.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv1-12.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv1-13.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv1-13.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv1-14.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv1-14.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv1-15.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv1-15.service ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /var/lib/gunbc/fleet-converge ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o briansrls -g briansrls /opt/actions-runner ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o briansrls -g briansrls /opt/actions-runner-cache diff --git a/provisioning/srv3/gunbc-ghrunner.sudoers b/provisioning/srv3/gunbc-ghrunner.sudoers index a9c1866fc93..31a83e0eeae 100644 --- a/provisioning/srv3/gunbc-ghrunner.sudoers +++ b/provisioning/srv3/gunbc-ghrunner.sudoers @@ -74,6 +74,11 @@ ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-10 ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-11 ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-12 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-13 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-14 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-15 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-16 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv3-17 ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv3-01.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv3-01.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv3-02.service @@ -96,6 +101,16 @@ ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv3-11.s ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv3-11.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv3-12.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv3-12.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv3-13.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv3-13.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv3-14.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv3-14.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv3-15.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv3-15.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv3-16.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv3-16.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv3-17.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv3-17.service ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o root -g root /opt/fabric-cells ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o root -g root /opt/fabric-cells/srv3-06 ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/fabric-cells/srv3-06/attempts @@ -104,7 +119,7 @@ ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl set-property fabric-cell-srv3-0 ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl set-property fabric-cell-srv3-06.slice MemorySwapMax=34359738368 ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl set-property fabric-cell-srv3-06.slice TasksMax=16384 ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl set-property fabric-cell-srv3-06.slice CPUWeight=100 -ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl set-property fabric-cell-srv3-06.slice CPUQuota=1000% +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl set-property fabric-cell-srv3-06.slice CPUQuota= ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start fabric-cell-srv3-06.slice ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 0700 -o root -g root /var/lib/gunbc/fabric ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 0700 -o root -g root /var/lib/gunbc/fabric/allocation diff --git a/provisioning/srv4/gunbc-ghrunner.sudoers b/provisioning/srv4/gunbc-ghrunner.sudoers index e4edae3bc35..2ff4d1bcbf6 100644 --- a/provisioning/srv4/gunbc-ghrunner.sudoers +++ b/provisioning/srv4/gunbc-ghrunner.sudoers @@ -75,6 +75,11 @@ ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-10 ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-11 ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-12 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-13 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-14 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-15 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-16 +ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /opt/actions-runner/srv4-17 ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv4-01.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv4-01.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv4-02.service @@ -99,6 +104,16 @@ ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv4-11.s ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv4-11.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv4-12.service ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv4-12.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv4-13.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv4-13.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv4-14.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv4-14.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv4-15.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv4-15.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv4-16.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv4-16.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl enable actions-runner@srv4-17.service +ghrunner ALL=(root) NOPASSWD: /usr/bin/systemctl start actions-runner@srv4-17.service ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o ghrunner -g ghrunner /var/lib/gunbc/fleet-converge ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o briansrls -g briansrls /opt/actions-runner ghrunner ALL=(root) NOPASSWD: /usr/bin/install -d -m 755 -o briansrls -g briansrls /opt/actions-runner-cache From 2f86af9ffead6268e1c4515d86188e0fbf0d52ba Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 19 Sep 2026 15:47:45 +0000 Subject: [PATCH 2/4] Merge main (heal_candidate.dag); mark the existing-quota repair paths blocked; narrow the % clause - Merge origin/main so the heal job's dag/gunbc/heal_candidate.dag entry resolves on this branch. - F-R1/F-R2: the CPUQuota= reset is a declared representation. Clearing a previously installed finite quota is EXPLICITLY BLOCKED on both paths, with cause and trigger named at gunbc.fabric_cell_effect (MemberChanged -> CellChangeNotYetRealizable; trigger: change_realization admitting a bounded resource-boundary change) and gunbc.host_converge (CPU drop-in not in the actuated managed set, no CPUQuotaPerSecUSec readback verdict; trigger: enrol + bind readback). No new apply machinery. - F-R3: only the CPU-survey refusal path is deleted; the memory-side unknown/zero conflation in gunbc_runner_slots_per_host keeps its owner and RunnerWidthResolution trigger. Reason row: 'no finite ceiling is declared; the reset is emitted'. - review 68479: the sudoers negative is a line-terminal %. Co-Authored-By: Claude Opus 5 (1M context) --- dag/gunbc/fabric/fabric_cell_effect.dag | 27 +++++++++++++---- dag/gunbc/host/host_converge.dag | 19 ++++++++++-- dag/gunbc/runner/runner_slot_allocation.dag | 29 ++++++++++++++----- ...utor_privileged_operation_witness_test.dag | 7 +++-- .../fabric_cell_converge_witness_test.dag | 3 +- 5 files changed, 66 insertions(+), 19 deletions(-) diff --git a/dag/gunbc/fabric/fabric_cell_effect.dag b/dag/gunbc/fabric/fabric_cell_effect.dag index 950eb9de0d6..8136923d2dc 100644 --- a/dag/gunbc/fabric/fabric_cell_effect.dag +++ b/dag/gunbc/fabric/fabric_cell_effect.dag @@ -106,12 +106,27 @@ fn fabric_cell_base_directory_effect() -> FabricCellEffect { // THE QUOTA LINE IS EITHER A CEILING OR AN AFFIRMATIVE UNSET, NEVER ABSENT. CpuQuotaResolved // renders `CPUQuota=%`; SlotCpuQuotaUnbounded renders `CPUQuota=` through // slice_cpu_quota_unbounded, which is the empty assignment systemd reads as "no quota". Omitting -// the line under Unbounded would be the widen DESIGN 5 forbids in the one case that matters: a -// cell that once carried a quota would keep it while the desired digest said infinity, and the -// reconcile would report MemberChanged forever. The RunnerSlotCpuQuota that used to carry a -// refusing arm no longer does (its only refusing producer, a vendor survey, is deleted), so this -// fold is total and the SliceDirectivesQuotaSurveyRefused / CellCpuQuotaSurveyRefused arms that -// carried that refusal are deleted with it rather than left as arms nothing can produce. +// the line under Unbounded would be the widen DESIGN 5 forbids: an ADDED cell would carry +// whatever quota its slice already had while the desired digest said infinity. The +// RunnerSlotCpuQuota that used to carry a refusing arm no longer does (its only refusing producer, +// a vendor survey, is deleted), so this fold is total and the SliceDirectivesQuotaSurveyRefused / +// CellCpuQuotaSurveyRefused arms that carried that refusal are deleted with it rather than left as +// arms nothing can produce. +// +// WHAT THIS DOES NOT DO, STATED SO THE RESET IS NOT READ AS A REPAIR (review F-R1, 2026-09-19). +// The six directives are rendered only for MemberAdded. An EXISTING boundary that differs from the +// desired row only by a finite CPU quota reconciles as MemberChanged, and every MemberChanged at +// this address is refused as CellChangeNotYetRealizable by fabric_cell_address_realization below -- +// so a previously installed finite quota is DETECTED (the digest compares "infinity" against the +// host's CPUQuotaPerSecUSec identity and reports drift) and is NOT cleared by this module. That is +// the inherited change-realization boundary, not a new one: gunbc.change_realization has not +// declared the staged intermediates for a resource-boundary change, and until it does the plan +// refuses rather than guessing. EXPLICITLY BLOCKED, with its trigger: the finite-to-unbounded +// repair on an existing cell lands when change realization admits a bounded resource-boundary +// change at this address -- the same subject, authorization and readback as the add path, applied +// through set-property to the existing slice, never by deleting and recreating the cell. The live +// fabric cell runs with CPUQuota infinity today (srv4 census), so nothing on the fleet sits behind +// this block; a cell that acquires a finite quota out of band would. // // WHY DIRECTIVES AND NOT A SystemdUnitFile: the effect's production realization is // `systemctl set-property =...` -- systemd renders and owns the persisted diff --git a/dag/gunbc/host/host_converge.dag b/dag/gunbc/host/host_converge.dag index 135b3d48834..0202c9ff69d 100644 --- a/dag/gunbc/host/host_converge.dag +++ b/dag/gunbc/host/host_converge.dag @@ -557,8 +557,23 @@ fn codex_runtime_bundle_knobs(host: HostIdentity) -> List { // EVERY REPRESENTABLE BOUNDARY EMITS A KNOB, INCLUDING THE ONE THAT MEANS NO CEILING. An arm that // converges successfully while realizing nothing is the fail-open review 57675 found; NoCpuCeiling // is not that arm, because "no ceiling" is a state a unit can fail to be in (one that was once -// given a quota keeps it), so it is realized as the affirmative empty assignment `CPUQuota=` that -// systemd reads as unset, through the same drop-in the ceiling would use. +// given a quota keeps it), so it is represented as the affirmative empty assignment `CPUQuota=` +// that systemd reads as unset, through the same drop-in the ceiling would use. +// +// A KNOB IS THE DESIRED REPRESENTATION, NOT ITS APPLICATION OR READBACK (review F-R2, 2026-09-19). +// Nothing in production carries this CPU drop-in through to a host today, for either arm: +// gunbc.host_axis_caps caps_managed_dropin_paths enrols the memory, swap, high-water and tasks +// drop-ins and not gunbc_runner_per_slot_cpu_dropin_path; gunbc.host_runner_memory_provision +// selects its three memory knobs and has no live apply; and gunbc.runner_unit_live_read +// converge_target_live_verdict answers a verdict only for the memory properties, so a CPUQuota +// target has no effective-value reader. This knob therefore does NOT clear a previously installed +// per-slot quota by production convergence; it declares what a converged unit must carry. +// EXPLICITLY BLOCKED, with its trigger: enrol the CPU drop-in path in the managed set the deploy +// apply authority actuates (set-property on the running unit, no restart), and bind its readback as +// a verdict over CPUQuotaPerSecUSec through parse_systemd_cpu_quota_per_sec_show -- the write is +// `CPUQuota=` and the satisfying read is `infinity`, two spellings that must stay distinct, and an +// unreadable property must not satisfy it. The live runner units run with CPUQuota infinity today +// (srv4 census), so nothing on the fleet sits behind this block. fn runner_cpu_boundary_knobs(h: RunnerHostDeployment, glob: String) -> List { let value = runner_cpu_boundary_directive_value(b: h.per_runner_cpu_boundary) [ diff --git a/dag/gunbc/runner/runner_slot_allocation.dag b/dag/gunbc/runner/runner_slot_allocation.dag index 9ccb7c711d4..f0b58be52fe 100644 --- a/dag/gunbc/runner/runner_slot_allocation.dag +++ b/dag/gunbc/runner/runner_slot_allocation.dag @@ -50,7 +50,7 @@ type HostRunnerSlotAllocation { } data gunbc_runner_slot_allocation_authority_disposition: Disposition = Terminal { - reason: "RE-DERIVED 2026-09-19 UNDER THE OPERATOR RULING 'there is no CPU demand - the processes we run are single core - do NOT overthink it please': cores_per_slot is an AUTHORED POLICY of 1 (gunbc_runner_cores_per_slot_policy), no longer derived from MemoryMax against a vendor memory-per-vCPU ratio, so the CPU axis admits 128 on every M128-30 host and STOPS BINDING; committed width is the memory-admitted width -- srv1 15, srv3/srv4 17 at the 26 GiB slot, fleet 49; and cpu_quota is SlotCpuQuotaUnbounded, so no CPUQuota= is rendered for a runner unit or a fabric cell slice (the hosts run unlimited today and a 100% quota would throttle parallel link/codegen). Every figure below that names a cores_per_slot of 6 or 10, a CPU admission of 21 or 12, or a CPU-bound committed width is PRIOR configuration retained as history.\n\nSingle authority for fleet runner-slot desired allocation (operator rulings 2026-07-10 superseding the 2026-07-05 brief; MemoryHigh-emit + swap-provisioning 2026-07-11; srv3 host-standup converge 2026-07-21; PER-HOST WIDTH + 14GiB slot 2026-08-05; srv4 disk row-pin retired + apply-time preflight 2026-08-06). Fleet-uniform per-slot row: 6 cores (DERIVED 2026-08-23 from the 16GiB MemoryMax at the surveyed 1 core : 3 GiB memory-per-vCPU convention, superseding an authored 8 that stated no relation to the ceiling beside it -- gunbc_runner_memory_per_core reads it from gunbc.runner_shape_census rather than restating it as a literal here) · 16GiB MemoryMax · 15GiB MemoryHigh (the authored max−1GiB relation, its own emitted knob per the 2026-07-11 ruling — a hit at 16GiB spills to swap after throttling at 15; raised from 14/13 by the 2026-08-17 floor-peak ruling in gunbc_runner_slot_memory_max_ruling_note) · 32GiB MemorySwapMax · TasksMax 16384 (the 2026-06-21 incident REPAIR value restored by operator ruling 2026-08-08 on PR 8045: TasksMax=512 WAS the diagnosed EAGAIN/process-creation outage value, and the earlier 512 row here inverted that diagnosis; any lower derived envelope value must be separately accepted and never below live task demand) · cpu_weight 100. PER-HOST width, RE-DERIVED 2026-08-27: MEMORY admits 25 on srv1 and 29 on srv3/srv4 at the 16GiB slot, and on srv2 IT IS UNESTABLISHED. CPU admits 21 on every host (128 cited cores at 6 per slot); disk admits nothing numeric anywhere. COMMITTED width is the minimum over those axes. SUPERSEDED 2026-09-13 BY THE 26/25 GiB TRANSITION: the slot is 26 GiB MemoryMax / 25 GiB MemoryHigh, cores_per_slot rises 6 -> 10, and CPU admits 12 on every host (128 cited cores at 10 per slot), so srv1/srv3/srv4 commit 12 and the fleet commits 36. srv2 NO LONGER HOLDS 5 AND HOLDS NOTHING: the held arm is deleted, its width RESOLVES TO A TYPED REFUSAL, and `gunbc.runner_host_deploy` constructs no operational deploy row for it at all. The figures in the rest of this row that describe the 16/15 slot and the 21-wide commitment are the PRIOR configuration and are retained only as the transition's starting point -- they are not the current standing and must not be cited as one.\n\nsrv1 MOVED 27 -> 25 AND THAT IS A SYNCHRONIZATION, NOT A NEW TARGET. The 27 was consistent with an ungrounded 20 GiB fleet-wide session figure; that figure is deleted and srv1 carries an evidence-backed 52 GiB allowance, which removes three slots from the derivation this literal claims to report. No committed width moves, because srv1's CPU axis binds at 21 either way.\n\nsrv2's HELD 5 IS DELETED (2026-09-13) AND WHAT REPLACED IT IS THE TYPED REFUSAL THIS NOTE ASKED FOR. The paragraph below states the eventual construction fix as replacing the Int API with a typed standing so an unestablished host cannot be read as a number at all; that is now done at the DEPLOY seam -- `RunnerHostDeployAdmission` constructs an operational row only in the admitted arm, so srv2 yields a cause and no row, and zero can no longer inhabit `RunnerHostDeploy`. The historical argument is kept because it is what motivated the construction; the held number it defends no longer exists. Its session reservation is unestablished, so the memory subtraction cannot be performed at all -- there is no denominator to subtract. A held number and a derived one are different claims: the first says what the host is currently running while it is re-grounded, the second says what the conservation wall would pass, and only the second is evidence. The previous revision of this sentence listed 5 beside three derived figures as though the four were the same kind of fact, which is exactly the reading the ruling forbids. test.claim.host_allocation_conservation the_srv2_width_derivation_is_unestablished is the executed statement of this; every_derived_width_matches_its_authored_copy deliberately excludes srv2 rather than counting it as reproduced. The eventual construction fix is to replace this Int API with a typed standing (MemoryWidthDerived / MemoryWidthUnestablished) so an unestablished host cannot be read as a number at all; until then this note is the boundary and the two witnesses are the wall. THE BINDING AXIS CHANGED AND THAT IS THE HEADLINE: memory bound every host in this fleet's entire history until 2026-08-22, and now binds only srv2, the one host whose 64GiB install failed training and was reverted. Everywhere else CPU binds, which is why host_memory_admitted_width reporting 29 is not a commitment to 29 (width_minimum_now_spans_three_axes_note). srv4's disk stays DiskWidthUnconstrained at allocation with the apply-time gate gunbc.runner_lifecycle runner_width_disk_preflight unchanged. Live fleet ran 5 slots on EVERY host when this was written, so srv3/srv4's sixth slot is a provisioning target rather than a description (gunbc_runner_slot_width_ruling_note). GUARANTEED mode is now STRUCTURAL rather than checked — runner_slice_cap is DERIVED as slots × MemoryMax instead of being a second literal, so Σ per-slot MemoryMax ≤ slice_cap holds by construction and an oversubscribed slice has no representation here (DESIGN 5 construction-over-validation; the prior 80GiB literal was kept coherent with 5 × 16GiB by hand, which the old note admitted when it called them 'coherent by construction at these values' — a coincidence maintained by an author, not a derivation). The real remaining constraint is CONSERVATION against host RAM, which is not this module's to check: gunbc.ci_runner_placement holds it, now against OS-usable RAM with the per-host session slice and per-host fixed overhead charged. Per-axis targets are operator-set; never reconcile this row toward live readback (live is not automatically truth — the 32-vs-0 swap fork existed precisely because the apply was assumed rather than readback-proven). Downstream: gunbc.ci_floor_measurement (declared vs live cap fork), gunbc.ci_runner_placement, gunbc.host_converge, gunbc.host_standup (prefix:host-converge + P1:runner-deploy-slot), gunbc.runner_slot_enforcement, gunbc.fleet_show_effective_read. ctrl applies host-side; gunbc emits converge — never apply from this module." + reason: "RE-DERIVED 2026-09-19 UNDER THE OPERATOR RULING 'there is no CPU demand - the processes we run are single core - do NOT overthink it please': cores_per_slot is an AUTHORED POLICY of 1 (gunbc_runner_cores_per_slot_policy), no longer derived from MemoryMax against a vendor memory-per-vCPU ratio, so the CPU axis admits 128 on every M128-30 host and STOPS BINDING; committed width is the memory-admitted width -- srv1 15, srv3/srv4 17 at the 26 GiB slot, fleet 49; and cpu_quota is SlotCpuQuotaUnbounded, so no finite CPU ceiling is declared for a runner unit or a fabric cell slice and the affirmative reset `CPUQuota=` is emitted in its place (the hosts run unlimited today per the srv4 census, and a 100% quota would throttle parallel link/codegen; the reset is a declared representation -- applying it to an EXISTING finite quota is explicitly blocked at gunbc.fabric_cell_effect and gunbc.host_converge, each naming its trigger). Every figure below that names a cores_per_slot of 6 or 10, a CPU admission of 21 or 12, or a CPU-bound committed width is PRIOR configuration retained as history.\n\nSingle authority for fleet runner-slot desired allocation (operator rulings 2026-07-10 superseding the 2026-07-05 brief; MemoryHigh-emit + swap-provisioning 2026-07-11; srv3 host-standup converge 2026-07-21; PER-HOST WIDTH + 14GiB slot 2026-08-05; srv4 disk row-pin retired + apply-time preflight 2026-08-06). Fleet-uniform per-slot row: 6 cores (DERIVED 2026-08-23 from the 16GiB MemoryMax at the surveyed 1 core : 3 GiB memory-per-vCPU convention, superseding an authored 8 that stated no relation to the ceiling beside it -- gunbc_runner_memory_per_core reads it from gunbc.runner_shape_census rather than restating it as a literal here) · 16GiB MemoryMax · 15GiB MemoryHigh (the authored max−1GiB relation, its own emitted knob per the 2026-07-11 ruling — a hit at 16GiB spills to swap after throttling at 15; raised from 14/13 by the 2026-08-17 floor-peak ruling in gunbc_runner_slot_memory_max_ruling_note) · 32GiB MemorySwapMax · TasksMax 16384 (the 2026-06-21 incident REPAIR value restored by operator ruling 2026-08-08 on PR 8045: TasksMax=512 WAS the diagnosed EAGAIN/process-creation outage value, and the earlier 512 row here inverted that diagnosis; any lower derived envelope value must be separately accepted and never below live task demand) · cpu_weight 100. PER-HOST width, RE-DERIVED 2026-08-27: MEMORY admits 25 on srv1 and 29 on srv3/srv4 at the 16GiB slot, and on srv2 IT IS UNESTABLISHED. CPU admits 21 on every host (128 cited cores at 6 per slot); disk admits nothing numeric anywhere. COMMITTED width is the minimum over those axes. SUPERSEDED 2026-09-13 BY THE 26/25 GiB TRANSITION: the slot is 26 GiB MemoryMax / 25 GiB MemoryHigh, cores_per_slot rises 6 -> 10, and CPU admits 12 on every host (128 cited cores at 10 per slot), so srv1/srv3/srv4 commit 12 and the fleet commits 36. srv2 NO LONGER HOLDS 5 AND HOLDS NOTHING: the held arm is deleted, its width RESOLVES TO A TYPED REFUSAL, and `gunbc.runner_host_deploy` constructs no operational deploy row for it at all. The figures in the rest of this row that describe the 16/15 slot and the 21-wide commitment are the PRIOR configuration and are retained only as the transition's starting point -- they are not the current standing and must not be cited as one.\n\nsrv1 MOVED 27 -> 25 AND THAT IS A SYNCHRONIZATION, NOT A NEW TARGET. The 27 was consistent with an ungrounded 20 GiB fleet-wide session figure; that figure is deleted and srv1 carries an evidence-backed 52 GiB allowance, which removes three slots from the derivation this literal claims to report. No committed width moves, because srv1's CPU axis binds at 21 either way.\n\nsrv2's HELD 5 IS DELETED (2026-09-13) AND WHAT REPLACED IT IS THE TYPED REFUSAL THIS NOTE ASKED FOR. The paragraph below states the eventual construction fix as replacing the Int API with a typed standing so an unestablished host cannot be read as a number at all; that is now done at the DEPLOY seam -- `RunnerHostDeployAdmission` constructs an operational row only in the admitted arm, so srv2 yields a cause and no row, and zero can no longer inhabit `RunnerHostDeploy`. The historical argument is kept because it is what motivated the construction; the held number it defends no longer exists. Its session reservation is unestablished, so the memory subtraction cannot be performed at all -- there is no denominator to subtract. A held number and a derived one are different claims: the first says what the host is currently running while it is re-grounded, the second says what the conservation wall would pass, and only the second is evidence. The previous revision of this sentence listed 5 beside three derived figures as though the four were the same kind of fact, which is exactly the reading the ruling forbids. test.claim.host_allocation_conservation the_srv2_width_derivation_is_unestablished is the executed statement of this; every_derived_width_matches_its_authored_copy deliberately excludes srv2 rather than counting it as reproduced. The eventual construction fix is to replace this Int API with a typed standing (MemoryWidthDerived / MemoryWidthUnestablished) so an unestablished host cannot be read as a number at all; until then this note is the boundary and the two witnesses are the wall. THE BINDING AXIS CHANGED AND THAT IS THE HEADLINE: memory bound every host in this fleet's entire history until 2026-08-22, and now binds only srv2, the one host whose 64GiB install failed training and was reverted. Everywhere else CPU binds, which is why host_memory_admitted_width reporting 29 is not a commitment to 29 (width_minimum_now_spans_three_axes_note). srv4's disk stays DiskWidthUnconstrained at allocation with the apply-time gate gunbc.runner_lifecycle runner_width_disk_preflight unchanged. Live fleet ran 5 slots on EVERY host when this was written, so srv3/srv4's sixth slot is a provisioning target rather than a description (gunbc_runner_slot_width_ruling_note). GUARANTEED mode is now STRUCTURAL rather than checked — runner_slice_cap is DERIVED as slots × MemoryMax instead of being a second literal, so Σ per-slot MemoryMax ≤ slice_cap holds by construction and an oversubscribed slice has no representation here (DESIGN 5 construction-over-validation; the prior 80GiB literal was kept coherent with 5 × 16GiB by hand, which the old note admitted when it called them 'coherent by construction at these values' — a coincidence maintained by an author, not a derivation). The real remaining constraint is CONSERVATION against host RAM, which is not this module's to check: gunbc.ci_runner_placement holds it, now against OS-usable RAM with the per-host session slice and per-host fixed overhead charged. Per-axis targets are operator-set; never reconcile this row toward live readback (live is not automatically truth — the 32-vs-0 swap fork existed precisely because the apply was assumed rather than readback-proven). Downstream: gunbc.ci_floor_measurement (declared vs live cap fork), gunbc.ci_runner_placement, gunbc.host_converge, gunbc.host_standup (prefix:host-converge + P1:runner-deploy-slot), gunbc.runner_slot_enforcement, gunbc.fleet_show_effective_read. ctrl applies host-side; gunbc emits converge — never apply from this module." } data gunbc_runner_slot_memory_max_ruling_note: String = "WHY 16GiB/15GiB, SUPERSEDING THE 14GiB/13GiB ROW (operator ruling 2026-08-17). The 2026-08-05 derivation below is not repudiated — it is re-run on a changed input. Its subject is floor-job cgroup peaks, and the floor's peak moved: the claim-scope cut measures 15.6GB (14.53GiB) after taking whole-run peak from 45.4GB to 15.6GB. 14.53GiB does not fit a 14GiB MemoryMax and sits 1.53GiB above a 13GiB MemoryHigh, so the prior row can no longer hold its own consumer.\n\nTHE PIN IS OBSERVED, NOT PREDICTED (srv1, read 2026-08-17, gunbc_srv1_converged_slot_throttle_observation). All five srv1 slots carry the declared 14GiB/13GiB. Two of them — srv1-04 and srv1-05 — report memory.peak of EXACTLY 13.00GiB against 829 and 44803 memory.high events, with memory.max events and oom_kill both zero. A peak equal to the throttle line is the censored lower bound this very note taught for the four 15.0GiB pins in the 2026-08-05 sample; it is the workload being held, not the workload being measured. Survival without OOM is reclaim plus swap, and the host carried 10GiB of swap in use at the same reading.\n\nWHAT IT COSTS, STATED BECAUSE IT IS NOT FREE: two runner slots fleet-wide. At 16GiB the managed hosts admit 6 rather than 7 (7 x 16 = 112GiB against 111.4GiB available) while srv1/srv2 hold 5 (5 x 16 = 80GiB against 81.0GiB, a 1.0GiB margin — the tightest number in this module and the first thing to recheck if the sessions reservation or the legacy overhead is ever corrected upward). Fleet total 24 -> 22. The trade is bought deliberately: a 15GiB/14GiB row would have preserved all 24 slots and cleared MemoryMax, but its throttle line would sit 0.53GiB BELOW the measured peak, making the throttle confound permanent and structural in the instrument now measuring the fold.\n\nTHAT THE NEW TARGET EQUALS srv2-03'S DRIFTED READBACK IS A COINCIDENCE AND MUST NOT BE READ AS A PRECEDENT. gunbc_runner_slot_allocation_refuses_effective_readback_reconciliation still stands unamended: this row moved because a floor-peak measurement moved, which is the same subject the 2026-08-05 ruling used, and it would have moved to 16GiB had every host in the fleet been sitting at 8. srv1 was CONVERGED to 14/13 when this was written, so the fleet was not at 16/15 to be reconciled toward.\n\nDISSOLVE-ON: the 14.53GiB peak is not established as steady. If the fold's remaining 5.6GB comes down, re-derive from the new floor-peak distribution and reclaim the two slots; this row is sized to a measurement under active reduction, not to a settled envelope.\n\nSUPERSEDED DERIVATION, RETAINED AS THE PRIOR INPUT (operator ruling 2026-08-05). 186 floor-job cgroup peaks from 2026-08-05 plus five sampled days back to 2026-07-16, read from the [calibration] floor_peak_post receipts the ci job emits: successful runs peak at p50 10.32GiB and max 10.96GiB, and NOT ONE run in the sample reached the 16GiB MemoryMax. Four of 186 reached 15.0GiB, which is the 15GiB MemoryHigh exactly — those are throttle pins, censored lower bounds rather than demand, and they are why the cap does not fall further.\n\nTHE FLOOR UNDER IT IS ALREADY MEASURED AND IT IS 12GiB: gunbc_floor_minimum_viable_armed_budget in this module, enforced by claim_executor as an arm-time refusal. MemoryHigh is what the run-time governor arms against, so MemoryHigh is the axis that number constrains — 13GiB leaves exactly 1GiB of margin over it, and clears the worst p90 in the three-week sample (12.94GiB on 2026-07-30). Going to 13/12 to buy an eighth slot on the managed hosts would put the throttle line ON the measured refusal floor with zero margin, and the 2026-07-30 distribution says that day would have spent most of its runs in swap.\n\nWHAT MOVING 16 TO 14 IS NOT: it is not oversubscription and does not touch the guaranteed-mode property. Every slot still owns its ceiling outright. The ceilings are simply sized closer to what the workload was measured to want, which is what freed the width." @@ -614,13 +614,26 @@ fn runner_slot_shape_standing(d: RunnerSlotDesired) -> RunnerSlotShapeStanding { } } -// THE DERIVED WIDTH CAN NO LONGER BE UNRESOLVED. Until 2026-09-19 the CPU axis read a vendor -// survey that could fail, and this function mapped that failure to 0 -- a declared gap whose -// dissolution trigger was a typed RunnerWidthResolution carried to every consumer. The gap -// dissolved from the other side: the only unresolvable input was deleted with the survey path, so -// every axis in the minimum is total and there is no arm left to conflate with zero. The -// zero-memory refusal above is the one remaining zero, and it is a refusal by construction, not a -// default. +// UNKNOWN AND ZERO ARE DIFFERENT ANSWERS, AND THIS FUNCTION STILL CONFLATES THEM ON THE MEMORY +// SIDE. Until 2026-09-19 there were two unresolvable inputs: the CPU axis read a vendor survey that +// could fail, and this function mapped that failure to 0. THAT PATH IS DELETED with the survey +// (gunbc_runner_cores_per_slot_policy), so the CPU axis is total. The memory side is unchanged: a +// host whose session charge is unestablished (srv2) yields a zero envelope, a zero memory +// admission and a zero minimum, and this Int carries that unestablished condition as a scalar 0 +// that a consumer cannot tell from a grounded zero. gunbc.ci_runner_placement contains the harm at +// the deploy seam -- admit_or_refuse_zero_width turns it into a named refusal -- but the two +// consumers that read the Int directly (gunbc.runner_host_deploy's deploy rows, +// gunbc.host_axis_caps's roster bound) still read an unestablished host as a real zero. +// +// DECLARED GAP, NOT AN ACCIDENT, and the trigger is the CAPABILITY rather than an artifact: the +// conflation dissolves when the canonical width answer is a RunnerWidthResolution -- derived, or +// unresolved-with-cause -- and every production call site carries or refuses that cause instead of +// reading an Int. That is a fail-closed cascade including `data` rows whose declared shape has no +// arm to match on, so it is the whole subject of its own change rather than a rider on this one; +// landing half of it would mint one silent default per un-migrated site. The population is named +// by its producer rather than transcribed: every non-comment, non-import occurrence of +// gunbc_runner_slots_per_host( under dag/gunbc and src/v2, less this declaration (DESIGN 6: name +// the instrument, never transcribe its output). fn gunbc_runner_slots_per_host(host: HostIdentity) -> Int { match runner_slot_shape_standing(d: gunbc_runner_slot_desired()) { RunnerSlotShapeRefusedZeroMemoryMax => 0 diff --git a/dag/test/claim/executor_privileged_operation_witness_test.dag b/dag/test/claim/executor_privileged_operation_witness_test.dag index cb1e0463ff3..8f9b32e2d6d 100644 --- a/dag/test/claim/executor_privileged_operation_witness_test.dag +++ b/dag/test/claim/executor_privileged_operation_witness_test.dag @@ -175,7 +175,10 @@ test fn witness_population_tracks_the_slot_roster() -> Bool { // committed file, or declare a different quota than the committed projection, and either this // or the generated-artifact gate goes false. The unbounded arm asserts the OTHER direction too: no // percentage grant may survive in the file, because a 100% quota at one core per slot is the -// throttle the ruling forbids. +// throttle the ruling forbids. That negative is a LINE-TERMINAL `%`: a percentage grant is the +// only sudoers line that ends in one, whereas a `%group` principal line begins with it, so the +// clause names the CPU grant and not every future row that happens to carry the character +// (review 68479). test fn witness_fabric_slice_cpu_quota_grant_is_the_declared_directive() -> Bool { let ds = fabric_cell_slice_desired_directives() let desired_lines = join(map(ds, d => systemd_slice_directive_line(directive: d)), "\n") @@ -189,7 +192,7 @@ test fn witness_fabric_slice_cpu_quota_grant_is_the_declared_directive() -> Bool let assignment = systemd_slice_directive_line(directive: slice_cpu_quota_unbounded()) string_contains(s: srv3_runner_host_sudoers(), pattern: join([assignment, "\n"], "")) && string_contains(s: desired_lines, pattern: assignment) - && !string_contains(s: srv3_runner_host_sudoers(), pattern: "%") + && !string_contains(s: srv3_runner_host_sudoers(), pattern: join(["%", "\n"], "")) } } } diff --git a/dag/test/claim/fabric/fabric_cell_converge_witness_test.dag b/dag/test/claim/fabric/fabric_cell_converge_witness_test.dag index c8801c1eaf4..f0ff6fe2f13 100644 --- a/dag/test/claim/fabric/fabric_cell_converge_witness_test.dag +++ b/dag/test/claim/fabric/fabric_cell_converge_witness_test.dag @@ -640,7 +640,8 @@ test fn witness_drifted_quota_standing_is_not_converged() -> Bool { // exactly as fabric_cell_boundary_address_probe does. RED: a desired digest that spelled unbounded // as a literal beside the parser's ("" or "unbounded") reads every live cell as drifted; the drift // test above is the other half -- a host that reports a one-thread quota against the same row is -// not converged, which is the throttle the 2026-09-19 ruling forbids showing up as drift-to-repair. +// reported as drift, which is the throttle the 2026-09-19 ruling forbids being made VISIBLE; its +// repair on an existing cell is explicitly blocked (gunbc.fabric_cell_effect, review F-R1). test fn witness_an_unbounded_host_quota_is_satisfied_under_the_unbounded_row() -> Bool { match systemd_cpu_quota_per_sec_identity(q: parse_systemd_cpu_quota_per_sec_show(raw: "infinity")) { Absent => false From 61ee5083d56522b9e709e388d5b3acd3cce04202 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 19 Sep 2026 17:12:13 +0000 Subject: [PATCH 3/4] Scope the quota-census statement to its subject (wording only) The srv4 runner-unit census is evidence about srv4's runner units; other units and the fabric boundary need their own readback, and this PR does not establish fleet-wide quota conformity. Co-Authored-By: Claude Opus 5 (1M context) --- dag/gunbc/fabric/fabric_cell_effect.dag | 7 ++++--- dag/gunbc/host/host_converge.dag | 5 +++-- dag/gunbc/runner/runner_slot_allocation.dag | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/dag/gunbc/fabric/fabric_cell_effect.dag b/dag/gunbc/fabric/fabric_cell_effect.dag index 8136923d2dc..6b8cec49945 100644 --- a/dag/gunbc/fabric/fabric_cell_effect.dag +++ b/dag/gunbc/fabric/fabric_cell_effect.dag @@ -124,9 +124,10 @@ fn fabric_cell_base_directory_effect() -> FabricCellEffect { // refuses rather than guessing. EXPLICITLY BLOCKED, with its trigger: the finite-to-unbounded // repair on an existing cell lands when change realization admits a bounded resource-boundary // change at this address -- the same subject, authorization and readback as the add path, applied -// through set-property to the existing slice, never by deleting and recreating the cell. The live -// fabric cell runs with CPUQuota infinity today (srv4 census), so nothing on the fleet sits behind -// this block; a cell that acquires a finite quota out of band would. +// through set-property to the existing slice, never by deleting and recreating the cell. The +// reported srv4 runner-unit census found unlimited quotas; the fabric boundary +// (fabric-cell-srv3-06.slice) requires its own readback, and this row does not establish +// fleet-wide quota conformity -- a cell found carrying a finite quota sits behind this block. // // WHY DIRECTIVES AND NOT A SystemdUnitFile: the effect's production realization is // `systemctl set-property =...` -- systemd renders and owns the persisted diff --git a/dag/gunbc/host/host_converge.dag b/dag/gunbc/host/host_converge.dag index 0202c9ff69d..0cb4f9e277a 100644 --- a/dag/gunbc/host/host_converge.dag +++ b/dag/gunbc/host/host_converge.dag @@ -572,8 +572,9 @@ fn codex_runtime_bundle_knobs(host: HostIdentity) -> List { // apply authority actuates (set-property on the running unit, no restart), and bind its readback as // a verdict over CPUQuotaPerSecUSec through parse_systemd_cpu_quota_per_sec_show -- the write is // `CPUQuota=` and the satisfying read is `infinity`, two spellings that must stay distinct, and an -// unreadable property must not satisfy it. The live runner units run with CPUQuota infinity today -// (srv4 census), so nothing on the fleet sits behind this block. +// unreadable property must not satisfy it. The reported srv4 runner-unit census found unlimited +// quotas; other runner units require their own readback, and this row does not establish +// fleet-wide quota conformity -- a unit found carrying a finite quota sits behind this block. fn runner_cpu_boundary_knobs(h: RunnerHostDeployment, glob: String) -> List { let value = runner_cpu_boundary_directive_value(b: h.per_runner_cpu_boundary) [ diff --git a/dag/gunbc/runner/runner_slot_allocation.dag b/dag/gunbc/runner/runner_slot_allocation.dag index f0b58be52fe..7ac4cfdc05a 100644 --- a/dag/gunbc/runner/runner_slot_allocation.dag +++ b/dag/gunbc/runner/runner_slot_allocation.dag @@ -50,7 +50,7 @@ type HostRunnerSlotAllocation { } data gunbc_runner_slot_allocation_authority_disposition: Disposition = Terminal { - reason: "RE-DERIVED 2026-09-19 UNDER THE OPERATOR RULING 'there is no CPU demand - the processes we run are single core - do NOT overthink it please': cores_per_slot is an AUTHORED POLICY of 1 (gunbc_runner_cores_per_slot_policy), no longer derived from MemoryMax against a vendor memory-per-vCPU ratio, so the CPU axis admits 128 on every M128-30 host and STOPS BINDING; committed width is the memory-admitted width -- srv1 15, srv3/srv4 17 at the 26 GiB slot, fleet 49; and cpu_quota is SlotCpuQuotaUnbounded, so no finite CPU ceiling is declared for a runner unit or a fabric cell slice and the affirmative reset `CPUQuota=` is emitted in its place (the hosts run unlimited today per the srv4 census, and a 100% quota would throttle parallel link/codegen; the reset is a declared representation -- applying it to an EXISTING finite quota is explicitly blocked at gunbc.fabric_cell_effect and gunbc.host_converge, each naming its trigger). Every figure below that names a cores_per_slot of 6 or 10, a CPU admission of 21 or 12, or a CPU-bound committed width is PRIOR configuration retained as history.\n\nSingle authority for fleet runner-slot desired allocation (operator rulings 2026-07-10 superseding the 2026-07-05 brief; MemoryHigh-emit + swap-provisioning 2026-07-11; srv3 host-standup converge 2026-07-21; PER-HOST WIDTH + 14GiB slot 2026-08-05; srv4 disk row-pin retired + apply-time preflight 2026-08-06). Fleet-uniform per-slot row: 6 cores (DERIVED 2026-08-23 from the 16GiB MemoryMax at the surveyed 1 core : 3 GiB memory-per-vCPU convention, superseding an authored 8 that stated no relation to the ceiling beside it -- gunbc_runner_memory_per_core reads it from gunbc.runner_shape_census rather than restating it as a literal here) · 16GiB MemoryMax · 15GiB MemoryHigh (the authored max−1GiB relation, its own emitted knob per the 2026-07-11 ruling — a hit at 16GiB spills to swap after throttling at 15; raised from 14/13 by the 2026-08-17 floor-peak ruling in gunbc_runner_slot_memory_max_ruling_note) · 32GiB MemorySwapMax · TasksMax 16384 (the 2026-06-21 incident REPAIR value restored by operator ruling 2026-08-08 on PR 8045: TasksMax=512 WAS the diagnosed EAGAIN/process-creation outage value, and the earlier 512 row here inverted that diagnosis; any lower derived envelope value must be separately accepted and never below live task demand) · cpu_weight 100. PER-HOST width, RE-DERIVED 2026-08-27: MEMORY admits 25 on srv1 and 29 on srv3/srv4 at the 16GiB slot, and on srv2 IT IS UNESTABLISHED. CPU admits 21 on every host (128 cited cores at 6 per slot); disk admits nothing numeric anywhere. COMMITTED width is the minimum over those axes. SUPERSEDED 2026-09-13 BY THE 26/25 GiB TRANSITION: the slot is 26 GiB MemoryMax / 25 GiB MemoryHigh, cores_per_slot rises 6 -> 10, and CPU admits 12 on every host (128 cited cores at 10 per slot), so srv1/srv3/srv4 commit 12 and the fleet commits 36. srv2 NO LONGER HOLDS 5 AND HOLDS NOTHING: the held arm is deleted, its width RESOLVES TO A TYPED REFUSAL, and `gunbc.runner_host_deploy` constructs no operational deploy row for it at all. The figures in the rest of this row that describe the 16/15 slot and the 21-wide commitment are the PRIOR configuration and are retained only as the transition's starting point -- they are not the current standing and must not be cited as one.\n\nsrv1 MOVED 27 -> 25 AND THAT IS A SYNCHRONIZATION, NOT A NEW TARGET. The 27 was consistent with an ungrounded 20 GiB fleet-wide session figure; that figure is deleted and srv1 carries an evidence-backed 52 GiB allowance, which removes three slots from the derivation this literal claims to report. No committed width moves, because srv1's CPU axis binds at 21 either way.\n\nsrv2's HELD 5 IS DELETED (2026-09-13) AND WHAT REPLACED IT IS THE TYPED REFUSAL THIS NOTE ASKED FOR. The paragraph below states the eventual construction fix as replacing the Int API with a typed standing so an unestablished host cannot be read as a number at all; that is now done at the DEPLOY seam -- `RunnerHostDeployAdmission` constructs an operational row only in the admitted arm, so srv2 yields a cause and no row, and zero can no longer inhabit `RunnerHostDeploy`. The historical argument is kept because it is what motivated the construction; the held number it defends no longer exists. Its session reservation is unestablished, so the memory subtraction cannot be performed at all -- there is no denominator to subtract. A held number and a derived one are different claims: the first says what the host is currently running while it is re-grounded, the second says what the conservation wall would pass, and only the second is evidence. The previous revision of this sentence listed 5 beside three derived figures as though the four were the same kind of fact, which is exactly the reading the ruling forbids. test.claim.host_allocation_conservation the_srv2_width_derivation_is_unestablished is the executed statement of this; every_derived_width_matches_its_authored_copy deliberately excludes srv2 rather than counting it as reproduced. The eventual construction fix is to replace this Int API with a typed standing (MemoryWidthDerived / MemoryWidthUnestablished) so an unestablished host cannot be read as a number at all; until then this note is the boundary and the two witnesses are the wall. THE BINDING AXIS CHANGED AND THAT IS THE HEADLINE: memory bound every host in this fleet's entire history until 2026-08-22, and now binds only srv2, the one host whose 64GiB install failed training and was reverted. Everywhere else CPU binds, which is why host_memory_admitted_width reporting 29 is not a commitment to 29 (width_minimum_now_spans_three_axes_note). srv4's disk stays DiskWidthUnconstrained at allocation with the apply-time gate gunbc.runner_lifecycle runner_width_disk_preflight unchanged. Live fleet ran 5 slots on EVERY host when this was written, so srv3/srv4's sixth slot is a provisioning target rather than a description (gunbc_runner_slot_width_ruling_note). GUARANTEED mode is now STRUCTURAL rather than checked — runner_slice_cap is DERIVED as slots × MemoryMax instead of being a second literal, so Σ per-slot MemoryMax ≤ slice_cap holds by construction and an oversubscribed slice has no representation here (DESIGN 5 construction-over-validation; the prior 80GiB literal was kept coherent with 5 × 16GiB by hand, which the old note admitted when it called them 'coherent by construction at these values' — a coincidence maintained by an author, not a derivation). The real remaining constraint is CONSERVATION against host RAM, which is not this module's to check: gunbc.ci_runner_placement holds it, now against OS-usable RAM with the per-host session slice and per-host fixed overhead charged. Per-axis targets are operator-set; never reconcile this row toward live readback (live is not automatically truth — the 32-vs-0 swap fork existed precisely because the apply was assumed rather than readback-proven). Downstream: gunbc.ci_floor_measurement (declared vs live cap fork), gunbc.ci_runner_placement, gunbc.host_converge, gunbc.host_standup (prefix:host-converge + P1:runner-deploy-slot), gunbc.runner_slot_enforcement, gunbc.fleet_show_effective_read. ctrl applies host-side; gunbc emits converge — never apply from this module." + reason: "RE-DERIVED 2026-09-19 UNDER THE OPERATOR RULING 'there is no CPU demand - the processes we run are single core - do NOT overthink it please': cores_per_slot is an AUTHORED POLICY of 1 (gunbc_runner_cores_per_slot_policy), no longer derived from MemoryMax against a vendor memory-per-vCPU ratio, so the CPU axis admits 128 on every M128-30 host and STOPS BINDING; committed width is the memory-admitted width -- srv1 15, srv3/srv4 17 at the 26 GiB slot, fleet 49; and cpu_quota is SlotCpuQuotaUnbounded, so no finite CPU ceiling is declared for a runner unit or a fabric cell slice and the affirmative reset `CPUQuota=` is emitted in its place (the reported srv4 runner-unit census found unlimited quotas -- other units and the fabric boundary require their own readback -- and a 100% quota would throttle parallel link/codegen; the reset is a declared representation -- applying it to an EXISTING finite quota is explicitly blocked at gunbc.fabric_cell_effect and gunbc.host_converge, each naming its trigger). Every figure below that names a cores_per_slot of 6 or 10, a CPU admission of 21 or 12, or a CPU-bound committed width is PRIOR configuration retained as history.\n\nSingle authority for fleet runner-slot desired allocation (operator rulings 2026-07-10 superseding the 2026-07-05 brief; MemoryHigh-emit + swap-provisioning 2026-07-11; srv3 host-standup converge 2026-07-21; PER-HOST WIDTH + 14GiB slot 2026-08-05; srv4 disk row-pin retired + apply-time preflight 2026-08-06). Fleet-uniform per-slot row: 6 cores (DERIVED 2026-08-23 from the 16GiB MemoryMax at the surveyed 1 core : 3 GiB memory-per-vCPU convention, superseding an authored 8 that stated no relation to the ceiling beside it -- gunbc_runner_memory_per_core reads it from gunbc.runner_shape_census rather than restating it as a literal here) · 16GiB MemoryMax · 15GiB MemoryHigh (the authored max−1GiB relation, its own emitted knob per the 2026-07-11 ruling — a hit at 16GiB spills to swap after throttling at 15; raised from 14/13 by the 2026-08-17 floor-peak ruling in gunbc_runner_slot_memory_max_ruling_note) · 32GiB MemorySwapMax · TasksMax 16384 (the 2026-06-21 incident REPAIR value restored by operator ruling 2026-08-08 on PR 8045: TasksMax=512 WAS the diagnosed EAGAIN/process-creation outage value, and the earlier 512 row here inverted that diagnosis; any lower derived envelope value must be separately accepted and never below live task demand) · cpu_weight 100. PER-HOST width, RE-DERIVED 2026-08-27: MEMORY admits 25 on srv1 and 29 on srv3/srv4 at the 16GiB slot, and on srv2 IT IS UNESTABLISHED. CPU admits 21 on every host (128 cited cores at 6 per slot); disk admits nothing numeric anywhere. COMMITTED width is the minimum over those axes. SUPERSEDED 2026-09-13 BY THE 26/25 GiB TRANSITION: the slot is 26 GiB MemoryMax / 25 GiB MemoryHigh, cores_per_slot rises 6 -> 10, and CPU admits 12 on every host (128 cited cores at 10 per slot), so srv1/srv3/srv4 commit 12 and the fleet commits 36. srv2 NO LONGER HOLDS 5 AND HOLDS NOTHING: the held arm is deleted, its width RESOLVES TO A TYPED REFUSAL, and `gunbc.runner_host_deploy` constructs no operational deploy row for it at all. The figures in the rest of this row that describe the 16/15 slot and the 21-wide commitment are the PRIOR configuration and are retained only as the transition's starting point -- they are not the current standing and must not be cited as one.\n\nsrv1 MOVED 27 -> 25 AND THAT IS A SYNCHRONIZATION, NOT A NEW TARGET. The 27 was consistent with an ungrounded 20 GiB fleet-wide session figure; that figure is deleted and srv1 carries an evidence-backed 52 GiB allowance, which removes three slots from the derivation this literal claims to report. No committed width moves, because srv1's CPU axis binds at 21 either way.\n\nsrv2's HELD 5 IS DELETED (2026-09-13) AND WHAT REPLACED IT IS THE TYPED REFUSAL THIS NOTE ASKED FOR. The paragraph below states the eventual construction fix as replacing the Int API with a typed standing so an unestablished host cannot be read as a number at all; that is now done at the DEPLOY seam -- `RunnerHostDeployAdmission` constructs an operational row only in the admitted arm, so srv2 yields a cause and no row, and zero can no longer inhabit `RunnerHostDeploy`. The historical argument is kept because it is what motivated the construction; the held number it defends no longer exists. Its session reservation is unestablished, so the memory subtraction cannot be performed at all -- there is no denominator to subtract. A held number and a derived one are different claims: the first says what the host is currently running while it is re-grounded, the second says what the conservation wall would pass, and only the second is evidence. The previous revision of this sentence listed 5 beside three derived figures as though the four were the same kind of fact, which is exactly the reading the ruling forbids. test.claim.host_allocation_conservation the_srv2_width_derivation_is_unestablished is the executed statement of this; every_derived_width_matches_its_authored_copy deliberately excludes srv2 rather than counting it as reproduced. The eventual construction fix is to replace this Int API with a typed standing (MemoryWidthDerived / MemoryWidthUnestablished) so an unestablished host cannot be read as a number at all; until then this note is the boundary and the two witnesses are the wall. THE BINDING AXIS CHANGED AND THAT IS THE HEADLINE: memory bound every host in this fleet's entire history until 2026-08-22, and now binds only srv2, the one host whose 64GiB install failed training and was reverted. Everywhere else CPU binds, which is why host_memory_admitted_width reporting 29 is not a commitment to 29 (width_minimum_now_spans_three_axes_note). srv4's disk stays DiskWidthUnconstrained at allocation with the apply-time gate gunbc.runner_lifecycle runner_width_disk_preflight unchanged. Live fleet ran 5 slots on EVERY host when this was written, so srv3/srv4's sixth slot is a provisioning target rather than a description (gunbc_runner_slot_width_ruling_note). GUARANTEED mode is now STRUCTURAL rather than checked — runner_slice_cap is DERIVED as slots × MemoryMax instead of being a second literal, so Σ per-slot MemoryMax ≤ slice_cap holds by construction and an oversubscribed slice has no representation here (DESIGN 5 construction-over-validation; the prior 80GiB literal was kept coherent with 5 × 16GiB by hand, which the old note admitted when it called them 'coherent by construction at these values' — a coincidence maintained by an author, not a derivation). The real remaining constraint is CONSERVATION against host RAM, which is not this module's to check: gunbc.ci_runner_placement holds it, now against OS-usable RAM with the per-host session slice and per-host fixed overhead charged. Per-axis targets are operator-set; never reconcile this row toward live readback (live is not automatically truth — the 32-vs-0 swap fork existed precisely because the apply was assumed rather than readback-proven). Downstream: gunbc.ci_floor_measurement (declared vs live cap fork), gunbc.ci_runner_placement, gunbc.host_converge, gunbc.host_standup (prefix:host-converge + P1:runner-deploy-slot), gunbc.runner_slot_enforcement, gunbc.fleet_show_effective_read. ctrl applies host-side; gunbc emits converge — never apply from this module." } data gunbc_runner_slot_memory_max_ruling_note: String = "WHY 16GiB/15GiB, SUPERSEDING THE 14GiB/13GiB ROW (operator ruling 2026-08-17). The 2026-08-05 derivation below is not repudiated — it is re-run on a changed input. Its subject is floor-job cgroup peaks, and the floor's peak moved: the claim-scope cut measures 15.6GB (14.53GiB) after taking whole-run peak from 45.4GB to 15.6GB. 14.53GiB does not fit a 14GiB MemoryMax and sits 1.53GiB above a 13GiB MemoryHigh, so the prior row can no longer hold its own consumer.\n\nTHE PIN IS OBSERVED, NOT PREDICTED (srv1, read 2026-08-17, gunbc_srv1_converged_slot_throttle_observation). All five srv1 slots carry the declared 14GiB/13GiB. Two of them — srv1-04 and srv1-05 — report memory.peak of EXACTLY 13.00GiB against 829 and 44803 memory.high events, with memory.max events and oom_kill both zero. A peak equal to the throttle line is the censored lower bound this very note taught for the four 15.0GiB pins in the 2026-08-05 sample; it is the workload being held, not the workload being measured. Survival without OOM is reclaim plus swap, and the host carried 10GiB of swap in use at the same reading.\n\nWHAT IT COSTS, STATED BECAUSE IT IS NOT FREE: two runner slots fleet-wide. At 16GiB the managed hosts admit 6 rather than 7 (7 x 16 = 112GiB against 111.4GiB available) while srv1/srv2 hold 5 (5 x 16 = 80GiB against 81.0GiB, a 1.0GiB margin — the tightest number in this module and the first thing to recheck if the sessions reservation or the legacy overhead is ever corrected upward). Fleet total 24 -> 22. The trade is bought deliberately: a 15GiB/14GiB row would have preserved all 24 slots and cleared MemoryMax, but its throttle line would sit 0.53GiB BELOW the measured peak, making the throttle confound permanent and structural in the instrument now measuring the fold.\n\nTHAT THE NEW TARGET EQUALS srv2-03'S DRIFTED READBACK IS A COINCIDENCE AND MUST NOT BE READ AS A PRECEDENT. gunbc_runner_slot_allocation_refuses_effective_readback_reconciliation still stands unamended: this row moved because a floor-peak measurement moved, which is the same subject the 2026-08-05 ruling used, and it would have moved to 16GiB had every host in the fleet been sitting at 8. srv1 was CONVERGED to 14/13 when this was written, so the fleet was not at 16/15 to be reconciled toward.\n\nDISSOLVE-ON: the 14.53GiB peak is not established as steady. If the fold's remaining 5.6GB comes down, re-derive from the new floor-peak distribution and reclaim the two slots; this row is sized to a measurement under active reduction, not to a settled envelope.\n\nSUPERSEDED DERIVATION, RETAINED AS THE PRIOR INPUT (operator ruling 2026-08-05). 186 floor-job cgroup peaks from 2026-08-05 plus five sampled days back to 2026-07-16, read from the [calibration] floor_peak_post receipts the ci job emits: successful runs peak at p50 10.32GiB and max 10.96GiB, and NOT ONE run in the sample reached the 16GiB MemoryMax. Four of 186 reached 15.0GiB, which is the 15GiB MemoryHigh exactly — those are throttle pins, censored lower bounds rather than demand, and they are why the cap does not fall further.\n\nTHE FLOOR UNDER IT IS ALREADY MEASURED AND IT IS 12GiB: gunbc_floor_minimum_viable_armed_budget in this module, enforced by claim_executor as an arm-time refusal. MemoryHigh is what the run-time governor arms against, so MemoryHigh is the axis that number constrains — 13GiB leaves exactly 1GiB of margin over it, and clears the worst p90 in the three-week sample (12.94GiB on 2026-07-30). Going to 13/12 to buy an eighth slot on the managed hosts would put the throttle line ON the measured refusal floor with zero margin, and the 2026-07-30 distribution says that day would have spent most of its runs in swap.\n\nWHAT MOVING 16 TO 14 IS NOT: it is not oversubscription and does not touch the guaranteed-mode property. Every slot still owns its ceiling outright. The ceilings are simply sized closer to what the workload was measured to want, which is what freed the width." From afcd6bc316ea7018b5d7f99b8116a64e8e1fa774 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 19 Sep 2026 19:17:49 +0000 Subject: [PATCH 4/4] Provision witnesses: the surplus fixture follows the committed width The five retirement rows drove srv4-13 / srv1-13 as the first surplus slot while the fleet committed 12. At 17/15 those are members, so the rows were exercising membership, not retirement, and went red on the required floor. The obligation itself is derived (runner_width_transition: committed + 1 .. prior_width), so only the fixtures move: srv4-18/19, srv1-16. 36/36 PASS scoped. Co-Authored-By: Claude Opus 5 (1M context) --- .../runner_slot_provision_witness_test.dag | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/dag/test/claim/runner/runner_slot_provision_witness_test.dag b/dag/test/claim/runner/runner_slot_provision_witness_test.dag index 475628ac62d..ba0e5034f6c 100644 --- a/dag/test/claim/runner/runner_slot_provision_witness_test.dag +++ b/dag/test/claim/runner/runner_slot_provision_witness_test.dag @@ -704,12 +704,19 @@ fn emits_mask(lines: List) -> Bool { any(lines, l => string_contains(s: l, pattern: "'mask'")) } +// THE SURPLUS FIXTURE INDICES FOLLOW THE COMMITTED WIDTH, NOT A TRANSCRIBED ONE. These rows drove +// srv4-13 and srv1-13 as the first surplus slots while the fleet committed 12; under the 2026-09-19 +// one-core-per-slot ruling srv4 commits 17 and srv1 15, so the required retirees derived by +// gunbc.runner_width_transition are srv4-18..21 and srv1-16..50, and a slot-13 fixture is a MEMBER +// whose plan is membership, not retirement. The fixtures name srv4-18 / srv4-19 / srv1-16 -- the +// first surplus index on each host -- so each row still exercises the retirement route. +// // 1. AN ACTIVE SURPLUS SLOT PRODUCES NO TREE REMOVAL. Ownership still refuses the incarnation, so // membership cannot authorize `rm -rf`. The inhibitor is a different obligation: a required retiree // the interruption authorization names is masked even while the unit is still serving. test fn an_active_surplus_slot_is_never_deleted() -> Bool { let lines = srv4_plan( - observed: [srv4_surplus_member(name: "srv4-13", active: true, enabled: true)], + observed: [srv4_surplus_member(name: "srv4-18", active: true, enabled: true)], evidence: srv4_evidence(standings: []), ) emits_no_tree_removal(lines: lines) @@ -720,11 +727,11 @@ test fn an_active_surplus_slot_is_never_deleted() -> Bool { // raise stays withheld after the inactive remainder is gone. test fn an_active_authorized_required_retiree_is_masked_and_not_deleted() -> Bool { let lines = srv1_plan( - observed: [srv1_surplus_member(name: "srv1-13", active: true, enabled: true)], + observed: [srv1_surplus_member(name: "srv1-16", active: true, enabled: true)], evidence: srv1_evidence(standings: [ runner_retirement_standing( o: retirement_observation_from_readings( - unit: "actions-runner@srv1-13.service" as NonEmptyStr, + unit: "actions-runner@srv1-16.service" as NonEmptyStr, unit_file_state: "enabled", active_state: "active", occupancy: CgroupObservedPopulated, @@ -737,11 +744,11 @@ test fn an_active_authorized_required_retiree_is_masked_and_not_deleted() -> Boo test fn an_unobserved_required_retiree_outside_membership_is_named_as_withheld() -> Bool { let lines = srv1_plan( - observed: [srv1_surplus_member(name: "srv1-13", active: true, enabled: true)], + observed: [srv1_surplus_member(name: "srv1-16", active: true, enabled: true)], evidence: srv1_evidence(standings: [ runner_retirement_standing( o: retirement_observation_from_readings( - unit: "actions-runner@srv1-13.service" as NonEmptyStr, + unit: "actions-runner@srv1-16.service" as NonEmptyStr, unit_file_state: "enabled", active_state: "active", occupancy: CgroupUnobserved { cause: "probe refused" }, @@ -753,7 +760,7 @@ test fn an_unobserved_required_retiree_outside_membership_is_named_as_withheld() && emits_no_tree_removal(lines: lines) && any(lines, l => string_contains(s: l, pattern: "__GUNBC_CONVERGE_WITHHELD__") - && string_contains(s: l, pattern: "actions-runner@srv1-13.service") + && string_contains(s: l, pattern: "actions-runner@srv1-16.service") ) } @@ -763,11 +770,11 @@ test fn an_unobserved_required_retiree_outside_membership_is_named_as_withheld() // predicate cannot see. test fn an_inactive_slot_with_a_populated_cgroup_is_not_deleted() -> Bool { let lines = srv4_plan( - observed: [srv4_surplus_member(name: "srv4-13", active: false, enabled: false)], + observed: [srv4_surplus_member(name: "srv4-18", active: false, enabled: false)], evidence: srv4_evidence(standings: [ runner_retirement_standing( o: retirement_observation_from_readings( - unit: "actions-runner@srv4-13.service" as NonEmptyStr, + unit: "actions-runner@srv4-18.service" as NonEmptyStr, unit_file_state: "masked", active_state: "inactive", occupancy: CgroupObservedPopulated, @@ -783,7 +790,7 @@ test fn an_inactive_slot_with_a_populated_cgroup_is_not_deleted() -> Bool { // realization actuates and defers. A pass that did both would be establishing its own postcondition. test fn a_permitted_surplus_slot_is_masked_and_not_deleted_on_the_first_pass() -> Bool { let lines = srv4_plan( - observed: [srv4_surplus_member(name: "srv4-13", active: false, enabled: false)], + observed: [srv4_surplus_member(name: "srv4-18", active: false, enabled: false)], evidence: srv4_evidence(standings: []), ) emits_mask(lines: lines) && emits_no_tree_removal(lines: lines) @@ -814,11 +821,11 @@ test fn a_pending_unmasked_standing_still_emits_the_inhibitor() -> Bool { // is removed -- and the mask is NOT re-emitted, because the unit is already retired. test fn a_completed_retirement_authorizes_exactly_that_tree_removal() -> Bool { let lines = srv4_plan( - observed: [srv4_surplus_member(name: "srv4-13", active: false, enabled: false)], + observed: [srv4_surplus_member(name: "srv4-18", active: false, enabled: false)], evidence: srv4_evidence(standings: [ runner_retirement_standing( o: retirement_observation_from_readings( - unit: "actions-runner@srv4-13.service" as NonEmptyStr, + unit: "actions-runner@srv4-18.service" as NonEmptyStr, unit_file_state: "masked", active_state: "inactive", occupancy: CgroupAbsent, @@ -826,7 +833,7 @@ test fn a_completed_retirement_authorizes_exactly_that_tree_removal() -> Bool { ), ]), ) - any(lines, l => string_contains(s: l, pattern: "-rf") && string_contains(s: l, pattern: "/opt/actions-runner/srv4-13")) + any(lines, l => string_contains(s: l, pattern: "-rf") && string_contains(s: l, pattern: "/opt/actions-runner/srv4-18")) && !emits_mask(lines: lines) } @@ -854,11 +861,11 @@ test fn a_completed_srv1_surplus_retirement_emits_the_teardown_argv() -> Bool { // 5. AN UNREADABLE CGROUP IS A NAMED REFUSAL, NOT A DELETION AND NOT A SILENCE. test fn an_unobserved_cgroup_withholds_the_removal_by_name() -> Bool { let lines = srv4_plan( - observed: [srv4_surplus_member(name: "srv4-13", active: false, enabled: false)], + observed: [srv4_surplus_member(name: "srv4-18", active: false, enabled: false)], evidence: srv4_evidence(standings: [ runner_retirement_standing( o: retirement_observation_from_readings( - unit: "actions-runner@srv4-13.service" as NonEmptyStr, + unit: "actions-runner@srv4-18.service" as NonEmptyStr, unit_file_state: "masked", active_state: "inactive", occupancy: CgroupUnobserved { cause: "probe refused" }, @@ -867,19 +874,19 @@ test fn an_unobserved_cgroup_withholds_the_removal_by_name() -> Bool { ]), ) emits_no_tree_removal(lines: lines) - && any(lines, l => string_contains(s: l, pattern: "actions-runner@srv4-13.service")) + && any(lines, l => string_contains(s: l, pattern: "actions-runner@srv4-18.service")) && !emits_mask(lines: lines) } // 6. A COMPLETED RECEIPT FOR A DIFFERENT SLOT CANNOT AUTHORIZE THIS ONE. The join is by exact -// identity; a gate that checked "some retirement completed" would delete srv4-13 on srv4-14's proof. +// identity; a gate that checked "some retirement completed" would delete srv4-18 on srv4-19's proof. test fn a_completed_receipt_for_another_slot_authorizes_nothing() -> Bool { let lines = srv4_plan( - observed: [srv4_surplus_member(name: "srv4-13", active: false, enabled: false)], + observed: [srv4_surplus_member(name: "srv4-18", active: false, enabled: false)], evidence: srv4_evidence(standings: [ runner_retirement_standing( o: retirement_observation_from_readings( - unit: "actions-runner@srv4-14.service" as NonEmptyStr, + unit: "actions-runner@srv4-19.service" as NonEmptyStr, unit_file_state: "masked", active_state: "inactive", occupancy: CgroupAbsent, @@ -905,7 +912,7 @@ test fn a_surplus_slot_outside_the_authorized_scope_is_neither_masked_nor_delete // planner supplies, so a route that has not been taught to observe retirement cannot delete or mask. test fn absent_evidence_emits_neither_mask_nor_removal() -> Bool { let lines = srv4_plan( - observed: [srv4_surplus_member(name: "srv4-13", active: false, enabled: false)], + observed: [srv4_surplus_member(name: "srv4-18", active: false, enabled: false)], evidence: no_retirement_evidence(), ) emits_no_tree_removal(lines: lines) && !emits_mask(lines: lines)