Repository navigation
Unbreak main: two flat-namespace collisions from #8129 - #8151
Conversation
main has been red since #8129 and every branch that merges it inherits the red. Two failures, one root cause, and the cause is NOT where it looks. `unresolved type 'Memory'` fired in dag/product/budget_tree.dag, dag/gunbc/ci_budget_tree.dag and dag/test/claim/budget_tree_witness_test.dag -- files dating to #6165 that were compiling. The obvious suspect was #8091, which added 213 lines to dag/std/measure.dag where `Memory` lives. That was a red herring: measure.dag's `Memory` lines are untouched. #8129 added a SECOND declaration spelled `Memory` -- a variant of BoardCapability in dag/product/compute_board/intent.dag -- and adding a name to the flat namespace is a corpus-wide edit, so `Memory` became ambiguous and every module importing it from std.measure as a type stopped resolving. The same PR added `unsatisfied_capabilities` to dag/product/compute_board/composition.dag, where gunbc.auth.github_credential already declared one, which is the second failure. THE NEWER NAME YIELDS, per the single-authority rule: `Quantity.Memory` in std.measure is the older and broader authority with consumers across the tree, and `unsatisfied_capabilities` in the auth module has five declaration sites and two consumers in heal_push_plan. So BoardCapability.Memory becomes MemorySubsystem -- accurate on an axis whose sibling constructor is already MemoryCapacity -- and the board fold becomes unsatisfied_board_capabilities. Nine call sites across four files; no behaviour changes and no emitted seed file mentions either name. Green by execution: compute_board_readiness, github_app_registry and budget_tree witnesses all pass here and all three fail on the base. WHAT THIS DOES NOT DO, stated so the repair is not mistaken for the wall. There is no mechanism preventing the next PR from doing exactly this again: the class is a flat-namespace homonym, which is decidable and therefore a construction wall waiting on the namespace lane's containment authority, not on diligence. Until that lands, this class recurs whenever two lanes add the same bare name, and it will keep arriving as an unrelated-looking build break in a third file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Independent execution receipt for this patch, from the main-is-red lane. Both target classes go to zero and nothing new appears. Whole-tree
The arithmetic closes: 84 = 74 + 8 + 2, so every remaining diagnostic is pre-existing rather than introduced here. Checked explicitly on the one that looked alarming — Confirming the diagnosis independently — both names are declared exactly twice on main: Two notes that may save the next person time: It reproduces only whole-tree. Entry-grain closures at 23, 64, 605 and 751 modules all compile clean, including the failing files themselves and the real gate entry The reproduction is cheap and needs no build: Renaming the newcomers rather than the — sent from gentle-dove-143 |
Main landed #8151, which fixes the same two #8129 flat-namespace collisions my 3ea09c1 fixed. Both conflicts are that duplicate, and both resolve to main's side: - intent.dag: main chose MemorySubsystem, I had chosen MemoryCapability. Main's landed, so mine dissolves. - compute_board_readiness_witness_test.dag: same rename, following. composition.dag needed no resolution — both sides independently picked unsatisfied_board_capabilities. My 3ea09c1 is therefore now redundant work; what survives from this branch is PRESS-0 step 1 and the claim_executor budget-diagnostic ordering, both verified present after the auto-merge. The four generated artifacts main touched (stage0 crate partition, stage0 emit plan, ci.yml, ci_spec) all resolved to main's side byte-for-byte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n merge CI on press-0 failed with `unresolved type 'ArtifactIdentity'` in three files this branch never touched — realize_kernel_test, hermetic_fixture_realization_test, reconcile_in_process_cache_test. The cause is a §3 name collision, the same class as #8129's `Memory`. `dag/std/cache_interface.dag` has carried `ArtifactIdentity<T>` for a long time; #8153 added a bare `ArtifactIdentity` in `src/v2/compiler/self_host/generation.dag`. Under namespace-only resolution the long-standing bare references stopped resolving uniquely, so the consumers of the ORIGINAL type broke while the new declaration compiled fine. main still carries it and no open PR is fixing it, so this is not a duplicate of someone else's repair — I checked first, having duplicated #8151 earlier today. The newer, narrower name moves: `SelfHostArtifactIdentity`. The cache-interface authority keeps its name and its consumers resolve again. Swept its three files together — the declaration, both self_host witnesses, and the field and function signatures that referenced it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n merge CI on press-0 failed with `unresolved type 'ArtifactIdentity'` in three files this branch never touched — realize_kernel_test, hermetic_fixture_realization_test, reconcile_in_process_cache_test. The cause is a §3 name collision, the same class as #8129's `Memory`. `dag/std/cache_interface.dag` has carried `ArtifactIdentity<T>` for a long time; #8153 added a bare `ArtifactIdentity` in `src/v2/compiler/self_host/generation.dag`. Under namespace-only resolution the long-standing bare references stopped resolving uniquely, so the consumers of the ORIGINAL type broke while the new declaration compiled fine. main still carries it and no open PR is fixing it, so this is not a duplicate of someone else's repair — I checked first, having duplicated #8151 earlier today. The newer, narrower name moves: `SelfHostArtifactIdentity`. The cache-interface authority keeps its name and its consumers resolve again. Swept its three files together — the declaration, both self_host witnesses, and the field and function signatures that referenced it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All three conflicts are the ArtifactIdentity collision I healed in d3e42a1, which main healed independently in #8177. Main chose GeneratedArtifactIdentity where I chose SelfHostArtifactIdentity, so all three files resolve to main's side and my rename dissolves. That is the second time today my repair of a main breakage raced a parallel fix (the first was #8151's Memory rename). Both times the collision was real and unfixed when I checked, and both times someone else was fixing it in the same window — the check that would actually prevent the duplicate work is looking for an in-flight PR touching the same symbol, not just the state of main. Verified after resolution: the belt work is intact (revision admission, tick partition, multiplicity arm all present), and the three generated artifacts main touched resolved to main's side byte-for-byte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ssions (#8132) * PRESS-0 step 3: dispatch admission asks whether a PROCESS is running, not whether a tmux name exists A retained remain-on-exit pane — the exact artifact the spawn path creates — answered "live" to belt_node_session_live, so a click returned DispatchAlreadyLive through the accepted band: a positive acknowledgement, no agent, and no refusal anywhere to count. That is worse than the selection refusal beside it, because an accepted no-op has zero observable frequency by construction (DESIGN §5, the absorbing fallback wearing an idempotence label). No new observer was built. The pane probe, the pane-to-evidence projection (worker_process_from_attempt_pane) and the per-session join (worker_process_for_session) already existed and already carried the refusal semantics, and the sessions UI route was already using them via belt_sessions_observe_for_instance. The defect was that the two ACTUATION sites — belt_tick_for_instance and belt_dispatch_node_for_instance — read the ls-only observation whose worker evidence is the placeholder whose own reason reads "tmux name presence does not observe pane liveness or exit". One concept had two answers in one module (§3); this routes both actuation sites through the answer that observes. Liveness is four-valued, not Boolean: Running may suppress a spawn; StaleTerminal is refused and never suppresses the new turn; LivenessUnobserved refuses rather than spawning blind, because a spawn under unknown liveness is how one node acquires two workers; Absent is the ordinary spawn path. Collapsing Unobserved into either pole is the state-space conflation that produced this defect one level down. Witnesses: the prior witness_node_session_live_true asserted exactly the behaviour that is now wrong, so it is replaced rather than kept beside the new arms. The discriminating red is witness_retained_dead_pane_is_not_live, which returned true under the old check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fix: render the exit code with to_string, the idiom this module and its sibling already use int_to_string exists only in src/v1/02_parse.dag, takes value: rather than n:, and no module under dag/gunbc/ calls it — the sole occurrence of int_to_string(n:) in that directory was the line this fixes. dispatch_process_cleanup_separation_note's own retained-pane message renders the same field as to_string(exit_code). Also restructures the liveness filter to the let-then-first form worker_process_for_session uses, rather than an inline parenthesized pipe-then-method that appears nowhere in the corpus. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Admit the two new dispatch arms into the wire-roster size pin dispatch_button_terminal_states maps over belt_dispatch_all_status_labels, so button totality and the terminal count both follow the roster by derivation and needed no edit. What failed is the third clause of witness_dispatch_button_states_total_over_wire: the absolute roster size, pinned so a new arm cannot enter the wire vocabulary without a conscious update to the surface that renders it. The subject genuinely grew by two (stale_session, session_liveness_unobserved), so the pin moves 8 -> 10. The pin is doing exactly its job here and is not being loosened: it is a controlled fixture over a hand-authored closed roster, and it stopped a wire-contract change from landing silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Announce a population budget refusal before the work that can swallow it A budget exhaustion killed the CI floor and said nothing. Run 31474198106 on PR #8132 ran the floor step for exactly 55m01s against gunbc_ci_ordinary_floor_budget_minutes = 55, exited 1, and emitted no OVER-BUDGET line anywhere in the attempt log. The step's own output stops mid-measurement three minutes before the process dies. The wall itself is correct and is not being weakened here — only its announcement is. A fail-closed mechanism whose diagnostic is missing is worse than a loud one: the refusal still fires, but readers cannot see why, so they reach for whatever number is nearby. In this incident that was a cgroup peak RSS reading sitting next to the silence, and the failure was diagnosed as a memory kill that never happened — by me, in the PR thread, before the operator corrected it. That is the cost being paid: the silence does not just withhold a cause, it manufactures a wrong one. TWO CANDIDATE LOSS MECHANISMS, both closed by this ordering without having to decide between them. The receipt write ran first and its path was interpolated into the message, so a stalled write under a loaded runner delays the only announcement past process death. And the message took an explicit stderr lock guard held across the write — a lock the main thread holds while streaming its own receipt phases, which is precisely what the floor was doing in the window this fired in. A watchdog must never wait on a resource held by the thread it polices. So the announcement now goes first: a ::error:: annotation on stdout so the cause reaches the run summary rather than only line ~7000 of a step log, the detail line on stderr, both flushed, and the receipt write afterwards where failing to write it can no longer suppress the diagnosis. NOT ESTABLISHED, stated so the next reader does not inherit a guess: which of the two mechanisms actually lost the message. Neither was reproduced. The fix is ordering that makes both harmless, not a repair of a located defect. Also merges origin/main (d3203de). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Heal main's two §3 name collisions from #8129 (compute_board) #8129 landed `Memory` as a `BoardCapability` variant and `unsatisfied_capabilities` in `product.compute_board.composition`. Both names already existed corpus-wide — `std.measure` `Memory` and `gunbc.auth.github_credential` `unsatisfied_capabilities` — so under namespace-only resolution the bare references stopped resolving uniquely and the whole-tree compile went red in four files: dag/gunbc/ci_budget_tree.dag unresolved type 'Memory' dag/product/budget_tree.dag unresolved type 'Memory' dag/test/claim/budget_tree_witness_test.dag dag/test/claim/github_app_registry_witness_test.dag 'unsatisfied_capabilities' not found It was invisible on main because every push run there was cancelled, so no floor run completed on the merge commit. The newer, narrower names move: `Memory` -> `MemoryCapability`, `unsatisfied_capabilities` -> `unsatisfied_board_capabilities`. The long-standing authorities keep their names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * PRESS-0 step 1: dispatch reads back the deployed revision before it acts gunbc.fleet_desired_observe fleet_revision_standing already decided the revision cell three ways, and its only consumer was gunbc.fleet_converge_cli. So convergence knew srv1 was drifted while the served page dispatched against the drifted tree anyway and said nothing — a worker spawned from code the fleet never admitted, reported as an ordinary spawn. The standing is now consulted FIRST in belt_dispatch_node_for_instance, before node lookup and before session observation, because the roster, the signoff and the command all come from that tree; refusing later would mean refusing after acting on it. Both non-converged arms refuse: DispatchRevisionDrifted carries BOTH revisions (the membership-diff `from` ruling — a catch-up needs the prior) and DispatchRevisionUnobserved carries its cause. Neither reaches the ok band. Serve maps drift to 409 and unobserved to 503. The repo path is instance.repo_root, not the srv1_gunbc_repo_root constant converge uses — hardcoding srv1 would mint a second path authority and answer for the wrong host on any other instance. SCOPE: the new witnesses are projection-grain — never-ok, loud band, drift names both revisions, unobserved names its cause. Whether the gate FIRES on a live drifted srv1 is a wet fact and is owed a wet receipt; it is not established here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Extend the band-fold vocabulary pin to the four new dispatch labels Run 31528394784 red batch 4: witness_band_fold_pins_operator_vocabulary pinned belt_dispatch_label_band_rows().length() == 8, and dashboard_instance_dispatch_contract_keystone_holds failed only as its conjunct. The four arms added by step 3 (stale_session, session_liveness_unobserved) and step 1 (deployed_revision_drifted, deployed_revision_unobserved) each get their band assertion; all four are loud, so the ok set is unchanged and witness_ok_labels_derive_from_band_fold still holds. This is the second roster I missed after adding arms — I fixed d4_wire_contract_labels and the button wire pin but not this one. Swept the class this time: the only other pin over these rosters is roadmap_sandbox_witness_test line 75, which is relative (all_status_labels().length() - 1) and self-adjusts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Extract the revision-admission fold, refuse duplicate sessions, delete the Boolean collapse Handback items 2, 3 and 5 from review artifact 51237. EXTRACT THE GATE (item 4). The revision check lived inline in dispatch, so every witness constructed DispatchRevisionDrifted directly and asserted its label, band and JSON — which would stay green if the fleet_revision_standing match were deleted outright. A test that survives removal of the thing it tests is not testing it. belt_revision_admission is now a pure FleetRevisionStanding -> BeltRevisionAdmission fold with the effectful read left at the call site, driven through all three arms by witness. It is also the shared seam the tick path will consume. 0/1/MANY (item 3). belt_node_session_liveness filtered by node id and took first(), so two sessions claiming one node resolved to whichever tmux listed first: [running, stale] answered running, [stale, running] answered stale. Same observation, two orders, two verdicts, no signal a choice was made. It now refuses with BeltSessionMultiplicityConflict naming every session, routed to a new dispatch arm, loud band, 409. The discriminating witness asserts order-independence directly. DELETE THE BOOLEAN (item 5). belt_node_session_live answered Bool over a four-state lifecycle, mapping StaleTerminal, LivenessUnobserved and Absent all to false though the module's own note says those three have different remedies. It had no production caller. Its four witnesses are re-pointed at belt_node_session_liveness and are STRONGER for it: each now asserts the exact arm rather than mere falsity. Rosters swept together — exemplars, d4_wire_contract_labels, the band-fold pin and the button wire pin all read 13. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Make the scheduled belt tick lifecycle-aware and revision-gated Handback P0s 1 and 2 from review artifact 51237: the dead-pane fix and the revision gate reached the click path only, and the timer is a dispatch actuator exactly as much as the button is. The tick passed the whole observed session list into belt_reconcile, which maps EVERY present row to an observed member and counts live.length() as occupied. So a retained dead pane stayed a member (desired + present = Unchanged, no respawn) AND consumed a capacity slot. It also never consulted the deployed revision, so a drifted host refused a click while spawning on a timer. Both paths now consume belt_revision_admission. A refused revision blocks spawn and reap ONLY — verification and publication continue, because they read independently bound receipts and already run when tmux observation refuses; stopping them would widen a spawn-admission refusal into a work stoppage. Cleanup precedes start, deferred one tick. A stale pane leaves observed membership, so its node becomes a spawn candidate — but spawning it this tick would collide with the tmux session name that still exists, because spawn runs before teardown. So the stale node's spawn is withheld and its teardown planned; the next tick observes it Absent and spawns normally. That is why this is a partition plus a deferral, not a filter. LivenessUnobserved and MultiplicityConflict are neither cleaned nor spawned. An unobserved pane might be running, so teardown could kill live work and spawning beside it could double-run the node. Witnessed at the belt grain rather than on a helper Boolean: the partition the tick feeds to belt_reconcile puts running, stale and unobserved in three different buckets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fix the witness import block I split in the wrong place (review 51277) CONFIRMED, and it was the worst possible failure for this PR. My edit inserted the fleet_desired_observe import in the MIDDLE of the roadmap_belt_actuate import list, closing that block one line early. So DispatchRevisionDrifted, DispatchRevisionUnobserved, belt_dispatch_result_ok, belt_dispatch_status_label, belt_dispatch_result_json_value, the label/band/exemplar roster accessors and the preflight symbols all sat inside `import gunbc.fleet_desired_observe { … }` while being defined in roadmap_belt_actuate.dag. The module could not resolve its imports, so NONE of the new evidence would have run — the revision-admission fold, the multiplicity conflict, the tick partition, all of it. A PR whose entire claim is "these gates are proven by execution" would have merged with zero executing witnesses. That is specification-without-execution exactly, and it is the failure mode I have been quoting at other people all session. fleet_desired_observe now exports only RevisionConverged / RevisionDrifted / RevisionUnobserved. The belt_actuate symbols are back in their own block, and I merged the two rather than leaving a split import authority for one module. Every imported symbol re-verified against its defining module by name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Make the tick honor its own lifecycle note (review 51285) BOTH FINDINGS CONFIRMED, and the first is the worse kind: the note described behaviour the code did not have. `classified.refused` was partitioned and never read again, so only stale node ids were withheld from spawn. A ready node with an unobserved-but-present session therefore read as ABSENT to belt_reconcile and could be spawned beside a pane that might be running — the exact double-run the click path's DispatchSessionLivenessUnobserved arm exists to prevent, and exactly what my own note said was prevented here. The deferral set is now every non-running class. MULTIPLICITY IS A NODE FACT, NOT A SESSION FACT, which is why the first cut missed it: belt_tick_classify_sessions read each row's process state in isolation, so two rows for one node (one running, one stale) landed in two different buckets and the reconciler acted on an ambiguity dispatch refuses. The partition is now node-aware — more than one row for a node is a conflict whatever the rows say individually — so both actuation paths refuse the same ambiguity. Witnessed: an unobserved session yields no running row and no spawn candidate; two rows for one node classify neither as running. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Heal main's ArtifactIdentity collision (#8153), inherited via the main merge CI on press-0 failed with `unresolved type 'ArtifactIdentity'` in three files this branch never touched — realize_kernel_test, hermetic_fixture_realization_test, reconcile_in_process_cache_test. The cause is a §3 name collision, the same class as #8129's `Memory`. `dag/std/cache_interface.dag` has carried `ArtifactIdentity<T>` for a long time; #8153 added a bare `ArtifactIdentity` in `src/v2/compiler/self_host/generation.dag`. Under namespace-only resolution the long-standing bare references stopped resolving uniquely, so the consumers of the ORIGINAL type broke while the new declaration compiled fine. main still carries it and no open PR is fixing it, so this is not a duplicate of someone else's repair — I checked first, having duplicated #8151 earlier today. The newer, narrower name moves: `SelfHostArtifactIdentity`. The cache-interface authority keeps its name and its consumers resolve again. Swept its three files together — the declaration, both self_host witnesses, and the field and function signatures that referenced it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Sweep GeneratedArtifactIdentity into the consumer #8177 left behind main is broken, not this branch. #8176 added dag/gunbc/self_host_artifact_materialization.dag importing ArtifactIdentity from v2.compiler.self_host.generation; #8177 then renamed that declaration to GeneratedArtifactIdentity without updating this consumer. My copy of the file is byte-identical to main's, so the compile-clean failure here is inherited rather than introduced. Swept the import, the return type, and both witness files together. THIS IS THE THIRD MAIN BREAKAGE OF THIS CLASS TODAY — Memory (#8129), ArtifactIdentity (#8153), and now the incomplete repair of that same rename. Two were new bare names colliding with existing ones, and this one is a rename that moved a declaration without its consumers. All three share a mechanism: a change is checked against the module it edits, while the breakage appears in modules it does not. Under namespace-only resolution that is mechanically decidable from the containment tree — a symbol that resolved before an edit and does not resolve after it is a whole-tree fact a lens can compute. Worth a lens rather than a fourth repair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Pin HTTP status for all five new dispatch arms (review 51368) CONFIRMED. roadmap_serve.dag maps the five new arms explicitly — stale session, revision drift and multiplicity conflict to 409, liveness unobserved and revision unobserved to 503 — but witness_dispatch_status_total still pinned only the original eight. A regression mapping any new arm to 200 would have stayed green. Worse than an absent pin, because status_map_red_note claims each refusal variant maps to ITS status so a collapse reds exactly one row. A reader checking whether the mapping is guarded found a sentence saying yes while the assertion had stopped covering half the arms. WHY THIS ONE WAS MISSED when the label, band, exemplar and button rosters were all swept: it lives in a different witness module and is keyed by CONSTRUCTED variants rather than by a roster the compiler counts, so nothing goes non-exhaustive when an arm appears. That asymmetry is the real defect and it is recorded on the note — until this assertion derives from the exemplar roster the way the label set does, it has to be swept by hand beside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Brian Searls <11205878+briansrls@users.noreply.github.com>
mainhas been red since #8129 and every branch that merges main inherits the red — including mine (#8115). Two failing classes, one root cause, and the cause is not where it looks.The misdirection
unresolved type 'Memory'fires indag/product/budget_tree.dag,dag/gunbc/ci_budget_tree.daganddag/test/claim/budget_tree_witness_test.dag— files dating to #6165 that were compiling fine. The obvious suspect is #8091, which added 213 lines todag/std/measure.dag, whereMemorylives. That is a red herring:measure.dag'sMemorylines are untouched by it.#8129 added a second declaration spelled
Memory— a variant ofBoardCapabilityindag/product/compute_board/intent.dag. Adding a name to the flat namespace is a corpus-wide edit, soMemorybecame ambiguous and every module importing it fromstd.measurein type position stopped resolving. The same PR addedunsatisfied_capabilitiestodag/product/compute_board/composition.dag, wheregunbc.auth.github_credentialalready declared one — that is the second failing class.The fix: the newer name yields
Per single authority:
Quantity.Memoryinstd.measureis the older and broader authority with consumers across the tree, andunsatisfied_capabilitiesin the auth module has five declaration sites plus two consumers inheal_push_plan. So:BoardCapability.MemoryBoardCapability.MemorySubsystemMemoryCapacityunsatisfied_capabilities(compute_board)unsatisfied_board_capabilitiesNine call sites across four files. No behaviour change; no emitted seed file mentions either name.
Evidence
Green by execution here, all three failing on the base:
compute_board_readiness_witness_test,github_app_registry_witness_test,budget_tree_witness_test.What this does NOT do
There is no mechanism preventing the next PR from doing this again. The class is a flat-namespace homonym — decidable, therefore a construction wall owed to the namespace lane's containment authority, not a matter of diligence. Until that lands it recurs whenever two lanes mint the same bare name, and it will keep arriving as an unrelated-looking build break in a third file. I did not add a lens here: a check for this belongs with that authority, not bolted beside a rename.
Opened from the import-deletion lane because it blocks that lane and every other; the names belong to the compute-board lane, so a different rename is entirely fine by me — the constraint is only that one of the two
Memorydeclarations has to give.