Repository navigation
Actuation phase 2: the endpoint-path state machine and the unlink admission - #7619
Conversation
… them Five defects the operator's post-merge review found in the merged tree, all model-only, no host mutation. 1. srv4 had no desired deployed-instance row, so a typed unavailability had no subject and the fleet's intent was four-fifths stated. Added, and grounded on a real observation rather than a formality: srv4 was read 2026-08-01 over the fleet-automation key the roster always intended. It holds no listener, no process, no unit, yet a stale server.sock remains on disk and its error log's whole contents are 'Address in use (os error 98)' -- a server born in a runner cgroup died with that cgroup without unlinking its socket, so every later server fails to bind and the cache tree holds one file. Distinct defect from srv1's stranded owner. 2. EndpointOwnerObserved carried cgroup and owner_context as independently writable fields that could disagree, with placement trusting the context. The cgroup path now lives inside each context arm as observed_cgroup: the context IS the classification of that cgroup, so the disagreement has nowhere to live. Retained as located diagnostic evidence, never a second input. 3. The session-capacity note claimed 50G while intended_capacity was none. The note was the true half -- ctrl-sccache.service does set 50G -- so the value now states it. CI capacity stays Absent deliberately and is now the only Absent, since 10 GiB is sccache's default rather than a decision. 4. readback_independence still classified --show-stats as ProbeMayEstablish and its authority note still asserted the refuted 'any sccache invocation auto-starts a server'. The earlier correction rewrote the mechanism prose but kept the arm, bridging the gap with 'still may-establish in the sense that matters here' -- the exact conflation the module exists to forbid, committed inside the module. Added ProbeAnswersWithoutSubject as its own arm: subject-creation and answer-without-subject carry different remedies (replace the probe vs ground it behind an existence check). 5. The hygiene-reaper scaffold bound BuildCacheServerPlacement to gunbc.host_build_cache_provision and named arms that do not exist (ServerUnitOwned, ServerLazySpawned). Repointed to gunbc.build_cache_instance with the real arms. Green by execution: 52 witnesses pass across the three affected modules (9 placement + 33 provision + 10 readback), all entries compile at 0 blocking errors. Three new controls pin the fourth arm: that it still corrects a positive verdict, that the two non-independent arms stay distinguishable through the fold, and that the cited probe now inhabits it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RunnerDeploySlot conflated materializing a slot with activating it, and only the second can create the birth defect: a materialized-but-disabled slot compiles nothing, while a slot that may accept work will connect-or-start on its first cache miss and, with no managed endpoint owner, leave the server inside its own cgroup as the host's de facto cache. So the prerequisite goes on ACTIVATION. gunbc.runner_activation owns that moment and consumes BuildCacheInstanceReady + HostCompilePoolReady; gunbc.runner_host_deploy keeps materialization and its readiness-blind runner_enable_command is DELETED, not deprecated -- leaving it would leave the old path constructible, and a wall with a door beside it is a sign. The two live in separate modules because the dependency runs one way: activation needs the deployed-instance model, materialization does not. Swapping P1/P1b in host_standup would NOT have done this. Ordering in a status list is a projection of a dependency, not the dependency; the enable command has to have no constructor that skips the receipt. Two corrections the controls forced, both worth recording: - admit_runner_activation originally never compared the receipt's unit to the instance's intended_unit, so a managed-unit-owned endpoint held by the WRONG unit would activate. witness_materializing_a_slot_does_not_activate_it caught it. ServerManagedUnitOwned means SOME unit holds it, which a legacy ctrl unit or a hand-started service satisfies. - The receipt first declared placement: ServerManagedUnitOwned, on the theory that naming one variant made the other four unwritable. This substrate widens a variant-typed field to its coproduct, so the field really had type BuildCacheServerPlacement and a runner-owned receipt was constructible -- the compiler said so, refusing a match as non-exhaustive over five arms. Shipping the note as written would have been rung inflation (DESIGN 4b) in the carrier meant to prevent it. The receipt now carries a bare managed_unit name and cannot hold a placement at all. Green by execution: 61 witnesses across four modules (9 new activation-wall controls + 10 runner-deploy + 9 placement + 33 provision), all entries at 0 blocking errors, publication gate ExitSuccess. The nine controls prove a bare host admission, an absent endpoint, both runner-owned lifecycles, unknown / unsupervised / ambiguous / unreachable placement, a receipt from another host, an unready compile pool, a partially verified instance, and a wrong unit all fail to yield an enable command -- and that materializing a slot still does not activate it. No host mutation. No producer for either receipt ships here, deliberately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # dag/gunbc/publication_grant.dag
…probe Two unrelated-to-this-PR repairs the floor demanded. ## The census pins were stale, and #7575 is where they went stale `the_surface_is_almost_entirely_derived` and `the_three_denominators_are_derived_and_distinct` had been failing on clean `origin/main` since 87fb808 (#7575), which added the `compile_dag_diagnostic_census` builtin arm to `v1_interpreter.rs` and did not move the numbers the census pins. The counts come from the `interpreter_dispatch_arm_rows` host builtin, which does not resolve outside the executor's discovery path, so they could not be read by inspection. They were instead derived by replicating the macro expansion over the `arm "identity" { "spelling" }` token lists the six rosters expand from. That replication reproduces all three previously-pinned numbers exactly at f2185ca (#7562, the prior interpreter change, which DID update its pins) -- three independent numbers matching is what makes the method trustworthy rather than a guess. It then puts the delta since at exactly one arm: one row, one identity, one spelling. The repaired pins were confirmed green by execution, not by the replication: claim_batch --entry dag/test/claim/v1_interpreter_primitive_surface_witness_test.dag PASS the_surface_is_almost_entirely_derived PASS the_three_denominators_are_derived_and_distinct PASS every_dispatch_site_is_enumerated PASS derived_and_declared_partition_the_surface derived 182 -> 183, declared 2 (unchanged) identities 173 -> 174, spellings 160 -> 161, sites 7 (unchanged) Why bumping the numbers is the honest repair rather than a ratchet being slackened: the failing witnesses pin absolute magnitudes, and their siblings assert the structural properties -- the partition law and the identities < rows < spellings ordering -- both of which stayed green throughout. `declared` stayed at 2, so the surface is still almost entirely derived and the witness name is still true. The new arm is derived from its own macro tokens like the other 172. Had #7575 added a hand-written declared arm, bumping `declared` would have hidden a regression, and this commit would have had to refuse instead. Sites stay 7 because the new arm lands at an existing dispatch symbol, which is the denominator that would have moved had the surface actually grown a site. ## The probe file `dag/test/claim/primitive_surface_probe_test.dag` was a throwaway used while locating the above; it was removed from the worktree but the dashboard's cadence auto-commit had already captured it in e5a8d58. `review 46534` caught it. It is baggage, not a witness -- it always returned true -- and it is deleted here rather than given assertions, because the assertions it would have carried are the ones the real census witnesses already make. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ission The first slice of the actuation bundle. Still no host mutation: this lands the model and the wall that a later mutating step must pass through. ## The defect it closes srv4, observed 2026-08-01: no listener, no process, no unit, a server.sock present on disk, and an sccache error log whose entire contents are "Address in use (os error 98)". A server born inside a runner cgroup died with that cgroup without unlinking its socket, so every later server fails to bind. The host has been uncached since roughly 2026-07-26 for that reason alone. `BuildCacheEndpointObservation.EndpointAbsent` cannot express this. It answers "is anybody listening", which is the right question for placement and the wrong one for actuation: under it, srv4 and a host that simply has no cache yet are the same state. They are not — one needs a unit started, the other needs a file removed FIRST and then a unit started, and starting the unit without removing the file reproduces error 98 forever. A state-space conflation whose measured cost is a host silently uncached for a week. `EndpointPathState` is therefore a separate axis from the owner: absent / listening / stale socket / path conflict / unavailable. ## Why the unlink is admitted rather than an ExecStartPre The obvious fix is `ExecStartPre=/bin/rm -f server.sock`, and it is an absorbing fallback. It unlinks in every path state rather than the one where unlinking is correct. Absent, it is a harmless no-op — which is what makes it look safe in testing. Conflict, it destroys a file this lane did not create. Listening — which happens on every restart of a healthy unit and every double-start race — it severs a live server's socket, leaving a running process no client can reach and that will not be replaced because it still holds memory and still parents compiles. Silent in exactly the way the stale socket was silent, so the remedy recreates the disease one layer up, and an unconditional rm never reports having removed something it should not have, so the deficit's frequency is zeroed by construction. `admit_stale_socket_unlink` admits only from `EndpointStaleSocket` under `ObservationCurrent`; every other state is a positively named, located refusal. The unlink is a separate admitted step before the unit start, able to refuse independently of it, never a line inside the unit. Three shapes deliberately avoided: - The admission takes the observed STATE, not a record of four Bools. An all-true evidence record is hand-writable and re-checks what the observation already decided — validation wearing construction's clothes, the same defect review 46217 found in the owner context. The four facts an observer must establish are stated as its contract, not re-represented as data. - `ObservationCurrencyUnknown` refuses exactly as `ObservationStale` does. Not knowing whether a read is current is the same evidence as knowing it is old, and an arm that graded them would fire precisely when /proc is least readable, which is when a bind race is likeliest. - A TCP endpoint refuses at admission rather than at command construction, so no admitted value exists for which no command can be built. ## Test plan Green by execution, 8 controls, 0 failures: claim_batch --entry dag/test/claim/build_cache_endpoint_path_test.dag PASS witness_the_srv4_stale_socket_is_the_one_state_that_unlinks PASS witness_a_live_listener_is_never_unlinked PASS witness_a_non_socket_path_is_never_unlinked PASS witness_an_absent_path_refuses_rather_than_no_ops PASS witness_an_unreadable_path_does_not_default_to_stale PASS witness_a_stale_read_cannot_license_an_unlink PASS witness_a_tcp_endpoint_has_no_stale_socket_state PASS witness_the_unconditional_sketch_differs_where_it_is_destructive `unconditional_unlink_sketch` is the RED control -- ExecStartPre=rm -f written as a predicate, answering true in all five states. Two witnesses assert the admission DIFFERS from it on the two destructive states rather than merely asserting the admission refuses: a refusal where the naive fix would also have been harmless proves nothing about the wall's worth. Per DESIGN section 4b the control stays enrolled rather than retiring once the wall lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses review 46581 finding 1, and lands the producer that finding made unavoidable. ## The defect (review 46581, real) admit_runner_activation bound the CACHE receipt to the activation two ways -- its instance host against the deploy host, and its managed_unit against the instance's intended_unit -- and gated the POOL receipt beside it on two Booleans alone. So a pool observed on a different host, or a correctly-observed slice with some other name, admitted activation. Same unbound-receipt defect the unit comparison exists to prevent, sitting in the sibling field. The asymmetry is how it survived being written: the receipt that got the attention got the binding. HostCompilePoolReady now carries its host, and two comparisons gate admission -- pool_receipt_is_for_host and pool_receipt_names_intended_pool. The slice is compared against BuildCacheInstance.intended_compile_pool rather than against gunbc_managed_compile_pool_slice: comparing to the global default would make the wall agree with a constant rather than with the instance being activated against, which is weaker and looks identical. ## The producer gunbc.host_compile_pool derives the receipt, so it stops being purely hand-authored. It deliberately adds NO sizing and NO topology: those already have an authority in gunbc.fleet_host_budget (CompilePoolPlacement, compile_pool_bytes_for, and extdeps.jobserver's jobserver_build_pool_bytes -- the same cited knob that bounds build concurrency). A HostCompilePool record with its own memory_high / memory_max / token_count fields would fork that and give a host two answers for how big its pool is. CompilePoolReadiness carries five distinct not-ready reasons. Two that look alike are kept apart on purpose: PoolNotDeclaredInTopology (the fleet has not decided it HAS a managed pool) versus PoolSliceAbsentOnHost (it has, and this host lacks it). Their remedies and owners differ -- a fleet-wide topology change that must shrink runner_pool in the same motion, versus provisioning one host -- so collapsing them would make the fleet decision read as a per-host chore. Sizing is checked LAST, after the slice is confirmed present and correctly named: reporting a sizing failure first would send an operator to look at installer configuration on a host with no slice to size, which is the wrong-location defect toolchain_ensure_reconcile_located_note already records. ## Test plan Green by execution, 18 witnesses across the two modules, 0 failures. The two new binding controls: PASS witness_a_pool_receipt_from_another_host_is_refused PASS witness_a_pool_receipt_naming_another_slice_is_refused Both are asserted rather than only the host case, because they fail for different reasons -- one receipt is about the wrong machine, the other about the wrong cgroup on the right machine -- and a wall catching only the first would still activate a runner whose compiles are charged outside the intended pool. The producer's load-bearing witness reads the REAL gunbc_compile_pool_placement row rather than a fixture: PASS witness_the_live_fleet_topology_yields_no_ready_pool It asserts FALSE. The row is CompilePoolInRunnerSlots, so the derivation returns not-ready for every host and activation refuses fleet-wide -- the correct answer today, and the one a hand-written all-true receipt silently contradicted. When the topology flips it reds deliberately, as the tripwire that the flip cannot land silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Finding 1 — the pool receipt was unbound. Correct, and it was my defect.
One deliberate choice worth flagging, since your finding named Two new controls, green by execution alongside the existing 16: Both, not just the host case: one receipt is about the wrong machine, the other about the wrong cgroup on the right machine, and a wall catching only the first would still activate a runner whose compiles are charged outside the intended pool. The finding also pushed the producer to land in the same commit — Finding 2 — the hand-rolled-predicate rule. I checked for it and can't find it.I looked before declining, and three things point the other way:
The lens that does exist in this area targets hand-unrolled folds over lists — a recursion written out by hand — which is a different shape from a total match over a closed coproduct. If there's a specific rule or lens I've missed, point me at the carrier and I'll fix it; I'd rather converge than argue the idiom. — sent from smart-stag-201 |
review 46621, non-blocking. This PR deleted runner_enable_command, and
gunbc.plans.fleet_subsumption_manual_gaps still named it in two rows --
the DESIGN section 3 citation-rot class, where a name-level claim in a
carrier goes false because something elsewhere was renamed or deleted
and nobody touched the row.
Only two of the six occurrences were stale. The other four deliberately
name the deleted symbol AS HISTORY and are left alone:
runner_enable_command_deleted_note records the deletion,
activation_readiness_blind_widen_sketch_note cites it as the RED
control's provenance ("a faithful copy of ... as it stood on main"), and
two notes explain why moving a row in host_standup would not have
sufficed. A citation of a deleted thing is only rot when it asserts the
thing still exists.
The two that did:
- The B1 gap's present-tense "models the runner ENABLE step
(runner_enable_command -> ...)" is now false; repointed to
runner_activate_command.
- The A-item's "Only THEN did the modeled part run: systemctl
daemon-reload + runner_enable_command" is a past-tense account of what
was hand-run on 2026-07-22, so it is recorded as the THEN-current path
rather than silently repointed -- the enable it describes is precisely
the readiness-blind one that produced this fleet's birth defect, and
rewriting it to name the new wall would erase the receipt. It now says
which path ran, that it is deleted, and what replaced it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
review 46625. Commit 8d19c16 added `host` to HostCompilePoolReady and updated two of the three construction sites; witness_ready_pool in runner_host_deploy_witness_test was the third and was missed. The reviewer is also right that this made the previous commit's test plan overclaim. That run was real -- 18 witnesses across host_compile_pool_test and runner_activation_wall_test -- but those are the two modules I edited, not the closure of the type I changed, so the third site was never compiled. Same miss as the ProbeEffectOnSubjectFold literal earlier in this lane: after changing a record or coproduct, enumerate every construction site of the TYPE, not the files in the diff. Now green by execution on the missed module too, all 10 witnesses including the three callers the review named. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…1-actuation # Conflicts: # dag/gunbc/runner_activation.dag # dag/test/claim/runner_activation_wall_test.dag # dag/test/claim/runner_host_deploy_witness_test.dag
|
And you're right that the previous test plan overclaimed. That run was real — 18 witnesses across Also in this push:
— sent from smart-stag-201 |
review 46655 finding 1, real and blocking. derive_compile_pool_readiness matched CompilePoolSliceObserved and bound `limits: _`, discarding the observed cgroup ceilings, then reported CompilePoolReady carrying the size derived from the JOBSERVER. So the receipt asserted a sized pool on the strength of a number nobody had compared to the host: a slice observed with a zero memory.max was declared ready against a nonzero requirement. Fabricated readiness rather than a missing check, and the distinction places the fix: the observation was already in hand and was thrown away, so this was never a gap awaiting a producer. It is fail-closed inverted at the one carrier whose whole job is deciding whether compiles have a bounded parent -- and an unbounded ceiling parenting every cache-miss compiler is precisely the defect this lane exists to close. Compared against memory_max rather than memory_high: max is the ceiling that actually bounds the cgroup, while high is a throttling watermark the kernel permits to be exceeded, so sizing against high would admit a slice whose hard limit sits below the compiles it must hold. THE FIXTURE WAS CERTIFYING THE HOLE, and that is recorded in-module rather than quietly deleted: observed_slice built all-zero limits and witness_a_declared_and_observed_slice_is_ready passed anyway. The fixture now carries a ceiling that can hold the derived size, and a new sibling pins the case it used to admit. Both halves are needed -- without the positive fixture the derivation could refuse everything and still pass; without the new one it could admit everything. Green by execution, 8 witnesses, 0 failures, including: PASS witness_a_slice_below_the_derived_size_is_not_ready Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Finding 1 — real, and my own fixture was certifying the hole
Worth being precise about the class: this is fabricated readiness, not a missing check. The observation was already in hand and was thrown away — so it was never a gap awaiting a producer, it was fail-closed inverted at the one carrier whose entire job is deciding whether compiles have a bounded parent. An unbounded ceiling parenting every cache-miss compiler is exactly the defect this lane exists to close, so getting it wrong here specifically is worse than getting it wrong anywhere else in the diff. Now compares And your pointer at Finding 2 — I checked harder this time, and I found the mechanism you're pointing at. It doesn't cover these.Last round I said I could find no such rule. That was under-researched, and two things I've since found are worth putting on the record because they cut partly your way:
So the discipline is real. What I can't establish is that it reaches these two functions, and one piece of evidence is decisive: On the disposition receipt: I'd rather not add one than add a false one. That leaves a genuine disagreement rather than an oversight, and I'd rather resolve it than keep restating it. If you can point me at either (a) the lens or gate that should fire on these and currently does not, or (b) the canonical fold surface for — sent from smart-stag-201 |
…sync the inert roster
Same merge class as the last push, caught this time BEFORE pushing by running the
whole-tree compile rather than only the marker census.
ContentHash home. Four more files main added import ContentHash from std.types,
which Phase A moved. Repointed onto the family member their values actually
inhabit, not a bare import swap:
- gunbc.guarantee_measurement probe_set_digest is folded from content_hash_atom
/ content_hash_tagged / content_hash_combine, so it is Fnv1a64Structural. Its
fold also had to move to the _structural variants, because Phase A DELETED the
union-level combine and content_hash_tagged takes a structural payload while
returning the union -- the union simply does not typecheck at this position.
- gunbc.stage0_rust_honest_frontier_projection and both witnesses carry the same
manifest_content_identity field as the previous batch; their "witness-abc123"
String casts became Fnv1a64Structural records with valid 16-hex digests.
Field rename collision. This branch renamed tested_base_tree -> tested_base_tree_hash
and merge_target_tree -> merge_target_tree_hash alongside the GitObjectId retype, and
main's #7583/#7619 lane added new callers on the old names. Updated those call sites
(dag/tools/merge_admission_current_context.dag and main's new tests in
merge_admission_attempt_witness_test.dag). FLAGGING FOR THE OPERATOR: the rename is
incidental to this PR's purpose -- only the retype was required -- and it is what
made these collisions instead of clean merges. It is kept because the record's
sibling field was already base_tree_hash, but it is a fair thing to ask me to revert.
The merge_admission witness import conflict resolved as the UNION: this branch's
three Phase A symbols plus main's two roster helpers and its new commit_workflow
import. All six are used in the file body and all six are declared.
Inert roster re-synced, both directions of the check firing as designed:
- DownstreamSupportRoster (#7571) is newly inert -- declared, named in plan prose,
constructed and read by nothing -- so it is rostered.
- AccessPolicy is no longer inert: main's std.effect_grant effect_grant_policy is a
real production consumer, so its row was STALE and is DELETED. That is exactly the
dissolve the row's own trigger specifies, not a hand-edit.
The roster now equals the live inert set exactly.
Verified on a binary rebuilt AFTER the merge (main touched src/v1, so the pre-merge
binary was not a valid oracle): whole-tree compile 0 blocking errors,
regen_divergence_count=0, and green suites -- merge_admission 28/28,
guarantee_measurement 9/9, honest_frontier 7/7, lifecycle_scaffold 28/28,
emitter poles 7/7, roadmap 39/39, c_compilation_unit 24/24, compilation_unit 15/15,
inert_carrier 4/4.
…7480) * Ground ContentHash on hash family (Phase A: carriers + R0 backstop) Rebased onto main. ContentHash becomes a family coproduct (Fnv1a64 | Sha256Hash | Sha1Hash) with where-refined hex carriers, compare_content_hash cross-family refusals, OCI algorithm-qualified digests, and construction walls (reviews 45496/45505). Co-authored-by: Cursor <cursoragent@cursor.com> * Fix review 45522 regressions and CI compile-gate timeout. - Restore guarantee-ladder and shell→intent authority in design_document.dag and guarantee_ladder_nodes/roadmap_focus in roadmap_authority.dag from main; keep intentional chash Fnv1a64Structural migration only. - Remove git_sha1 witness Absent fallback (mint must succeed or test reds). - Raise v1-compiler-tests compile-gate step budget to 10m (run 30646205825). Co-authored-by: Cursor <cursoragent@cursor.com> * chore: regenerate drifted generated artifacts (ci auto-heal) * Update DESIGN open-thread row for ContentHash Phase A landing (#7480). Replace the stale pre-migration §3 residue with Phase A receipts and an explicit Phase B dissolve-on remainder (union-level == typecheck refusal). Co-authored-by: Cursor <cursoragent@cursor.com> * WIP: Ground ContentHash on its hash family so a cross-family comparison is un * chore: regenerate drifted generated artifacts (ci auto-heal) * Move OCI digest algorithm predicates to extdeps.container.oci.digest. Review 45539 flagged OCI-specific algorithm grammar in std.content_hash as a §3 authority inversion. The predicates and where-refinement entry point now live at the OCI digest authority; infer imports oci_other_digest_algorithm from there and stage0 emits extdeps_container_oci_digest.rs. Co-authored-by: Cursor <cursoragent@cursor.com> * WIP: Ground ContentHash on its hash family so a cross-family comparison is un * WIP: Ground ContentHash on its hash family so a cross-family comparison is un * fix(stage0): commit emitted extdeps_container_oci_digest seed module Regen emits the OCI digest module but it was never tracked in git after rebase, which broke cargo fmt and the build gate on PR #7480. Co-authored-by: Cursor <cursoragent@cursor.com> * WIP: Ground ContentHash on its hash family so a cross-family comparison is un * fix(oci digest): realign seed parse path and emitter Optional Present arm Review 45614: use `encoded: encoded` in the other-algorithm parse arm (oci_encoded_digest returns NonEmptyStr?, not a record). Route split parts through oci_wire_digest_parts_from_split so the seed types as String. Fix v1 emitter construction so Present{value: <branded scalar cast>} lowers to Some(...) instead of Optional::String. Restore extdeps_base module path for extdeps_container_oci_digest. Co-authored-by: Cursor <cursoragent@cursor.com> * chore: regenerate drifted generated artifacts (ci auto-heal) * WIP: Ground ContentHash on its hash family so a cross-family comparison is un * WIP: Ground ContentHash on its hash family so a cross-family comparison is un * WIP: Ground ContentHash on its hash family so a cross-family comparison is un * WIP: Ground ContentHash on its hash family so a cross-family comparison is un * chore: regenerate drifted generated artifacts (ci auto-heal) * fix(emit-on-demand-host): refuse crypto ContentHash at structural projection Review 45661: replace emit_host_identity_structural sentinel collapse with typed outcome coproducts (EmitHostStructuralIdentityRefusedCrypto, ResolvedBuildContextIdentityRefusedCrypto, ToolchainIdentityRefusedCrypto). Distinct Sha256/Sha1 digests no longer share one fnv1a64 cache key. Witness resolved_build_context_identity_refuses_sha256_observed_tool exercises the refusal path; structural fixture tests route through outcome helpers. Co-authored-by: Cursor <cursoragent@cursor.com> * WIP: Ground ContentHash on its hash family so a cross-family comparison is un * test(emitter): fix optional Present witness include/exclude patterns The exclude Optional::String substring matches lawful return-type emission; use the broken-body pattern String { value: text instead so the witness discriminates Some(...) lowering from the generic Optional::String variant path. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(emit-on-demand-host): serialize ContentHash cache path segments Route native cache workspace path construction through serialize_content_hash instead of bare as String casts so cache keys use the canonical wire form (review 45713). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(emit-on-demand-host): drop parallel family field from crypto refusals ContentHash already carries its family; RefusedCrypto variants now hold only component and identity. Witness derives family via content_hash_family (review 45721). Co-authored-by: Cursor <cursoragent@cursor.com> * fix: restore doc-graph construction walls from main (review 45726) Reverts accidental regression of HandAuthoredDocBind primary_work + additional_works, merged accelerator-demo-roundtrip row, guarantee-recovery dual anchors, and doc_reachability construction witnesses (#7486/#7489). Updates sha256_digest_content_hash_convergence_note to match the construction-walled bridge (as_content_hash_cryptographic), not wire-form rendering prose. Co-authored-by: Cursor <cursoragent@cursor.com> * WIP: Ground ContentHash on its hash family so a cross-family comparison is un * fix(oci-digest): construction-wall OciOtherDigestEncoded payload OciOtherDigestBody.encoded is now NonEmptyStr where oci_other_digest_encoded (descriptor.md charset), not a forgeable bare NonEmptyStr (review 45731). Infer where-refinement enrollment mirrors oci_other_digest_algorithm. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(oci-digest): emit parse wire path without NonEmptyStr cast panic Use parts.encoded as OciOtherDigestEncoded after oci_encoded_digest validates (same pattern as sha512 hex), avoiding emitter panic on String→NonEmptyStr cast in the OtherDigest parse arm. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(oci-digest): OciOtherDigestEncoded as String where for emit parity NonEmptyStr where oci_other_digest_encoded made the emitter emit panic() on String→NonEmptyStr cast in parse_oci_content_digest_wire. String where oci_other_digest_encoded matches OciSha512DigestHex and keeps the construction wall (review 45731). Co-authored-by: Cursor <cursoragent@cursor.com> * WIP: gunbc oracle * WIP: gunbc oracle * fix(content-hash): project .digest at string boundaries; repair the last floor reds Three sites where a structural hash reached a String position: - roadmap_dispatch_actuator dispatch_attempt_key and roadmap_belt_actuate belt_uuid_hex32 cast the content_hash_atom / content_hash_combine_structural RESULT to String. content_hash_atom returns Fnv1a64Structural (a record) since the family grounding, so the cast no longer yields the digest text. Notably this is not a typecheck error -- roadmap_belt_actuate's own uuid witnesses passed while the value was wrong -- so the only witness that caught it was the one comparing a parsed branch against the minted one (attempts_parse_roundtrips_the_minted_branch). Both now project .digest, the idiom site/moodboard.dag already used. A regex + paren-matching sweep over every .dag confirms no structural result is cast directly to a string type anywhere. - source_integration_landing_spine's witness compared a receipt against the raw literal "multiplicity-receipt" after construction moved to content_hash_of_value(...), i.e. the field now holds a hash of that string. Verified by execution: dispatch_attempts 9/9, roadmap_belt_actuate uuid witnesses 3/3, source_integration_landing_spine 22/22. (roadmap_belt_actuate's seven belt_exec witnesses fail locally with "hermetic mode: no mock_response for operation Check" -- they shell out to a real host process and never reach hash code; that is the hermetic runner refusing to fabricate, not a regression.) * fix(content-hash): re-home ContentHash imports that arrived with the main merge The floor's dag_compile_clean_gate went red on the merge commit with one hard diagnostic: name 'ContentHash' not found in module 'std.types'. Two witness files came in from main still importing ContentHash from std.types and initializing it from a string literal -- the shape this PR's family grounding removed: dag/test/claim/check_coverage_admission_witness_test.dag (4 rows) src/v2/test/claim/walk_plan_schedule_lens_test.dag (1 row) Both now import from std.content_hash and mint through content_hash_of_value, matching their consumers, which take the union ContentHash (merge_admission check_coverage_admits; the walk-plan target field). My pre-merge sweep for this class was clean and stayed correct -- these files did not exist on this branch when it ran. The sweep has to be re-run after each merge, not once. Re-run now: zero std.types ContentHash imports and zero string-literal ContentHash initializers corpus-wide. Verified by execution: whole-tree `gunbc compile --target dag` reports 0 hard diagnostics (the gate's own check); check_coverage_admission 12/12; walk_plan_schedule_lens 2/2. * WIP: gunbc oracle * revert(v2-node): restore node.dag hash surface to its 2804c8b state The WIP auto-commit at 8212c2a captured a mid-edit working tree: a speculative retype of v2.std.node content_hash / symbol_identity_digest / bag_hash_digest / byte_offset_cache_key_fingerprint to return Hash, snapshotted BEFORE I had fixed a stray argument label it introduced -- fn symbol_identity_digest(sym: Symbol) -> Hash { structural: content_hash_atom(value: sym) # not an expression } -- so that broken form went out on the branch. My subsequent `git checkout --` restored to that bad commit rather than to the intended baseline, which is how it survived. This restores the file byte-for-byte to 2804c8b. The retype is reverted on its merits regardless of the label bug: executed against the three failing classes (Variant-cast, digest-on-String, atom_identity_hash arity) it fixed NONE of them, so it was an unverified edit to a load-bearing substrate file with no demonstrated benefit. The real roots are recorded in the PR comment: five `ContentHash where brand(...)` types that can no longer be cast, and the raw atom_identity_hash / std.content_hash dual surface in 02_parse and self_host. * WIP: gunbc oracle * chore: regenerate drifted generated artifacts (ci auto-heal) * Project the corrected ContentHash bridge claim into DESIGN.md sha256_digest_content_hash is the VALIDATING PARTIAL bridge Digest->ContentHash?; as_content_hash_cryptographic is the TOTAL injection. Regenerated via tools.generated_artifact_gate main_wet; byte-stable on a second pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * WIP: gunbc oracle * WIP: gunbc oracle * WIP: gunbc oracle * WIP: gunbc oracle * WIP: gunbc oracle * WIP: gunbc oracle * WIP: gunbc oracle * WIP: gunbc oracle * WIP: gunbc oracle * WIP: gunbc oracle * WIP: gunbc oracle * WIP: gunbc oracle * WIP: gunbc oracle * Restore the v1 seed sources an auto-commit reverted to main The session auto-commit fired while src/v1 was checked out to origin/main for a control build, snapshotting main's version as this branch's HEAD; a later 'git checkout HEAD -- src/v1/' then restored that poisoned state. Three files lost their reviewed content and are restored from f9dc5a8: 00_core.dag — 'predicate not enforced at compile time' advisory reason 04_infer.dag — lower_hex_{16,40,64,128} / oci_other_digest_* where-predicates and the structural content-hash rewrite 05_emit_rust.dag — variant_surface_name + fn_returns_optional 05_emit_rust.dag is reconstructed onto main's newer revision rather than overwritten, so main's rust_btree_set_ord name-grain childless gate and its note are preserved; the result is byte-identical to f9dc5a8 apart from that addition. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * WIP: gunbc oracle * Remove temporary emitter-probe fixture Diagnostic scaffold for isolating the optional-payload cast emit defect; the repro is recorded in the PR discussion, not enrolled as a witness pending a decision on the emitter fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Record the optional-payload cast emit deficit as executing evidence Three poles, each holding one variable constant, isolate the trigger for the E0433 that blocks finding 3's oci_encoded_digest shape: refined return, no cast -> lowers to Some(..) (refined return is harmless) plain String? return, cast inside payload -> broken (the cast alone suffices) cast hoisted to a let -> STILL broken (refutes the workaround) The third pole is load-bearing: hoisting does not avoid the defect, so no respelling both constructs the carrier and lowers correctly, and the emitter repair is required rather than optional. An earlier draft of this file claimed the hoist WAS a working scaffold spelling and briefly landed that spelling in oci_encoded_digest; executing it refuted the claim, and both are reverted -- oci_encoded_digest keeps the reviewed inline shape. Poles 2 and 3 are known-red receipts: they assert the deficit is present, so they pass now and red when the emitter is repaired. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * WIP: gunbc oracle * WIP: gunbc oracle * Repair optional-payload cast lowering; regenerate the seed from the authority Confirmed name-loss point (by instrumentation, not by reading): the concrete-type peel in v1.compiler.emit_rust emit_rust_expr_record_lit. A cast reaching an optional payload gives the literal a concrete resolved type, so the peel replaced the authored "Present" with the payload's concrete name; emit_typed_record_lit then missed the optional arm and fabricated "Optional::String { .. }", which is not a Rust constructor (E0433). A probe printing variant_name/tn/peeled at the call site showed the authored name arriving intact and being overwritten. The standing variant_surface_name hypothesis was wrong as a single cause but is a real SECOND contributor: with the peel fixed, optional_variant still keyed off the resolved struct name until variant_surface_name reached the seed. Both were required; neither alone sufficed. The peel is suppressed only when the authored name is a built-in optional constructor AND the concrete type does not itself declare a variant of that name, so a user-defined "Present" still peels (variant_belongs_to_enum finds it) and ordinary variants are untouched. Variant identity is preserved, never inferred from the payload type. Authority is src/v1/05_emit_rust.dag. The seed could not regenerate the fix, so minimal bootstrap edits were lifted by hand and then PROVEN by regeneration: regen_stage0 --verify reports regen_divergence_count=0, so committed stage0 is exactly the fresh self-compile of the authority. Also restores cli_run.rs, the v1_interpreter R0 cross-family guard, and two more hand-written seed files that the earlier auto-commit poisoning reverted to main (the R0 guard is merged onto main's newer interpreter, not overwritten). Witness flipped from known-red receipt to permanent regression control (DESIGN section 4b): 6/6 including the Wrap::Holds and user-defined-Present negative controls. Nested-refinement receipt retains its polarity, 2/2. Whole-tree compile exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Declare publication grants for the 17 ungranted added public paths The Stage-0 visibility guard (tools.publication_placement_gate, run as the Wet floor runnable publication_placement_gate_passes) diffs the publication cutover to HEAD and refuses any added public path with no Publish grant. It reported PublishGuardLocalViolation count=17, which is why the ci floor failed while build, regen and heal all passed. Six are this PR's: the OCI digest carrier, its generated stage0 projection, and the four ContentHash/emitter witness files. The other eleven arrived with main (the heal-revalidation and source-integration-proof-kernel lanes, ci_heal_dispatch, extdeps.github.workflows and the preflight emit pair); the guard diffs from the cutover rather than from the merge base, so it fires on the union and this branch cannot go green while they are undeclared. All seventeen are already merged and pushed to the public repository, so granting records the publication that already happened rather than newly exposing anything. Flagged rather than silent because eleven of the paths are not this PR's to classify. Gate re-run locally: ExitSuccess. publication_placement_gate_test 8/8. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * WIP: gunbc oracle * Converge the fnv1a64 dual surface at the 22 sites the re-key stranded The floor's discovery corpus surfaced 22 witnesses this PR broke, in two classes, both instances of the dual surface DESIGN section 3 tracks: v2 code calls the RAW v1 intrinsics while std.content_hash is the modeled surface over the same authority. Re-keying v2.std.node.Hash to Fnv1a64Structural made that fork observable, because a raw intrinsic yields a native String where the modeled carrier is a record. Class 1 (18) -- "cannot access field digest on String": raw atom_identity_hash flowed into Hash positions. Eleven call sites in v2.compiler.02_parse, v2.compiler.self_host and the parse_table_content_key witness now route through v2.std.node symbol_identity_digest, the modeled surface those modules already import. The substitution is value-preserving: content_hash_atom(v) is by definition structural_content_hash(digest: atom_identity_hash(v)). Class 2 (2) -- "atom_identity_hash requires exactly one string argument": the inverse, a structural passed INTO the intrinsic. v2.lens.interface_summary signature_fingerprint_of_node hashed a node content hash that is now a record; it projects .digest first. A direct cast was tried and rejected by the interpreter (cannot cast Record to Fnv1a64Structural), so the original hash-of-hash semantics are kept rather than changed. Plus one value mismatch: the live_deploy readiness witness compared site_artifact_digest(..) as String against the digests the served body embeds, which now project .digest. After this the raw intrinsics appear nowhere outside dag/std/content_hash.dag and the v1 builtin registry, which is the single-authority end state. Re-run green: parse_table_content_key 5/5, interface_summary_firewall 4/4, ingest_bridge 9/9, cross_language_add_python_to_typescript 4/4, inhabitant_neutralization 6/6, parse_table_memo_governed 9/9, self_host_realized_comparison_floor 6/6, parse_stamp_span_index 2/2, live_deploy readiness 25/25. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * WIP: gunbc oracle * Address review 2026-08-01: restore roadmap containment wall, narrow optional emitter, correct design authority Three blocking items from the review, each with executing evidence. 1. Roadmap containment wall (regression against the branch base). My earlier merge dropped main's rendered-node containment wall entirely: roadmap_rendered_ghost_count, both RED probes, and all three witnesses were absent from this head. Restored roadmap_spawner.dag from main wholesale (main was a strict superset there) and grafted the import block, note and three tests onto roadmap_authority_test.dag while keeping this PR's Fnv1a64Structural changes. 38/38 green, the count the review predicted. 2. Optional emitter classification was broader than its stated boundary. The fn_returns_optional disjunct is a fact about the ENCLOSING FUNCTION, so a user-defined MyOpt::Present built inside a `-> String?` function emitted Some(..), overriding an effective_parent that had already resolved correctly. Added the negative control the review asked for and CONFIRMED IT RED before changing anything. Then executed all seven poles with the function-wide arm removed: it was load-bearing for none of them, so it is deleted rather than gated -- resolved_type.return_cardinality == CardOptional is the expression-local statement of the same fact, and the disjunct was a second representation of it. 7/7 green; regen_divergence_count=0. 3. Design authority still described the deleted models. Corrected design_document.dag (v2.std.node Hash is Fnv1a64Structural, not std.types.ContentHash -- wrong home and wrong type, contradicting the Phase A row below it), regenerated DESIGN.md, and corrected both locked plan docs to say the unit carries one agnostic identity from which each realization derives its spelling. Corpus sweep for the three stale claims is clean. Also repairs a main red this branch inherited: CompileDiagnosticCensus became an unrostered inert carrier when main's codex-review-46144 repair deleted census_rows(c: CompileDiagnosticCensus), its only production consumer. The three files involved are byte-identical to main, so the red is main's, not this branch's. Rostered with that reason and the standard dissolve trigger. * Discharge the CR/NUL control gap: main's Unicode escapes fired its dissolve-on trigger Round 2 asked for hostile-spelling controls over / \ CR LF NUL and ". CR and NUL were declared as a coverage BOUNDARY rather than faked, because .dag string literals had no numeric-escape form: backslash-r lexed as literal backslash-then-r and the backslash-u brace form lexed as its raw characters, both measured at the time. The note carried an explicit dissolve-on trigger. Main landed Unicode string escapes (#7585), so the trigger fired. The escapes were RE-MEASURED on this head rather than assumed from the commit title: \u{000d} and \u{0000} each yield exactly one character, of codepoint 13 and 0. CR and NUL are now DIRECT witnesses (a%00000db, a%000000b) and both join the output-always-safe control, so the whole requested hostile set is witnessed against the real encoder with no argument from escape-branch genericity standing in for any member. The encoder needed no change -- the genericity argument the boundary rested on turned out to be correct, which is exactly what direct evidence was owed to show. 24/24 green. The note is kept as the receipt that the boundary was declared, triggered and discharged rather than quietly dropped. Also merges current main (4 commits). One conflict, in dag/extdeps/realization/artifact_store_fs.dag: this branch moved ContentHash to std.content_hash while main added std.access {Permit, Deny} from the access-kernel change. Resolved as the union -- both wanted, neither superseding the other. * WIP: gunbc oracle * Merge main (15 commits): repoint four more ContentHash consumers, re-sync the inert roster Same merge class as the last push, caught this time BEFORE pushing by running the whole-tree compile rather than only the marker census. ContentHash home. Four more files main added import ContentHash from std.types, which Phase A moved. Repointed onto the family member their values actually inhabit, not a bare import swap: - gunbc.guarantee_measurement probe_set_digest is folded from content_hash_atom / content_hash_tagged / content_hash_combine, so it is Fnv1a64Structural. Its fold also had to move to the _structural variants, because Phase A DELETED the union-level combine and content_hash_tagged takes a structural payload while returning the union -- the union simply does not typecheck at this position. - gunbc.stage0_rust_honest_frontier_projection and both witnesses carry the same manifest_content_identity field as the previous batch; their "witness-abc123" String casts became Fnv1a64Structural records with valid 16-hex digests. Field rename collision. This branch renamed tested_base_tree -> tested_base_tree_hash and merge_target_tree -> merge_target_tree_hash alongside the GitObjectId retype, and main's #7583/#7619 lane added new callers on the old names. Updated those call sites (dag/tools/merge_admission_current_context.dag and main's new tests in merge_admission_attempt_witness_test.dag). FLAGGING FOR THE OPERATOR: the rename is incidental to this PR's purpose -- only the retype was required -- and it is what made these collisions instead of clean merges. It is kept because the record's sibling field was already base_tree_hash, but it is a fair thing to ask me to revert. The merge_admission witness import conflict resolved as the UNION: this branch's three Phase A symbols plus main's two roster helpers and its new commit_workflow import. All six are used in the file body and all six are declared. Inert roster re-synced, both directions of the check firing as designed: - DownstreamSupportRoster (#7571) is newly inert -- declared, named in plan prose, constructed and read by nothing -- so it is rostered. - AccessPolicy is no longer inert: main's std.effect_grant effect_grant_policy is a real production consumer, so its row was STALE and is DELETED. That is exactly the dissolve the row's own trigger specifies, not a hand-edit. The roster now equals the live inert set exactly. Verified on a binary rebuilt AFTER the merge (main touched src/v1, so the pre-merge binary was not a valid oracle): whole-tree compile 0 blocking errors, regen_divergence_count=0, and green suites -- merge_admission 28/28, guarantee_measurement 9/9, honest_frontier 7/7, lifecycle_scaffold 28/28, emitter poles 7/7, roadmap 39/39, c_compilation_unit 24/24, compilation_unit 15/15, inert_carrier 4/4. * Merge main #7608: ground the new GitSha1ObjectId fixtures on Sha1Digest Phase A made GitObjectId's digest a Sha1Digest record rather than a raw hex String, and main's new commit_writer_admission witness constructs GitSha1ObjectId { digest: "1111..." } directly. Both fixtures now use the tree's existing idiom, Sha1Digest { hex: <40 hex> as Sha1DigestHex }, with the carriers imported from std.content_hash. The object_store.dag import conflict resolved as the union: this branch's Phase A content_hash block plus main's two new imports (PositiveInt, repeat_string). BLOCKED, and NOT worked around: dag/gunbc/commit_workflow.dag fails to compile with "no field 'path' on type 'T'" at commit_writer_index_entries_for_path. This is NOT this branch's defect and it is deliberately left alone rather than papered over with a speculative type annotation (DESIGN section 5: noticing you are implementing a workaround IS the line-stop signal). Evidence it is main's: - the file is BYTE-IDENTICAL to origin/main here; - it reds in its OWN narrow closure, not just whole-tree, so it is a real defect rather than a resolution artifact; - this branch's only 04_infer changes are where-predicate registrations (lower_hex_*, oci_other_digest_*), which cannot affect lambda parameter inference; - main's CI for #7608 (f3df2ce) was still QUEUED when this merge happened, and three of the preceding main commits are outright red, so the commit landed unverified. Hypotheses tested and REFUTED by execution, so the next person does not repeat them: trailing comma in the parameter list; filter over an imported record type (both compile clean in an isolated probe); a shadowing filter declaration; a lambda/parameter name colliding with an imported symbol; cross-call-site unification collapsing the element type (only two callers, both passing List<GitIndexEntry>). The sibling flat_map over the same type and the same `entry` binding compiles, so it is specific to this call. * WIP: gunbc oracle * Remove cw_probe.dag: a scratch bisect probe the auto-commit swept into the branch Not source. It was a copy of commit_workflow.dag under a probe module name, used to bisect the 'no field path on type T' failure to its cause, and it should never have been committed. * Unblock the compile-clean gate: declared scaffold for the filter name capture, two more ContentHash homes THE BLOCKER, root-caused by bisection rather than guessed. dag/gunbc/commit_workflow.dag failed with "no field 'path' on type 'T'". src/v2/std/algebra.dag declares fn filter<T>(xs: FreeMonoid<T>, predicate: fn(T) -> Bool). commit_workflow does NOT import v2.std.algebra -- but it imports v2.compiler.source_authority, which drags the whole v2 compiler closure including v2.std.algebra into the module POOL. The bare name filter then binds to that declaration instead of the builtin, List<GitIndexEntry> does not unify with FreeMonoid<T>, the lambda parameter stays unbound, and the module reds. Discriminating measurement: importing v2.std.algebra for `length` ALONE -- never filter -- reproduces it, and deleting only the source_authority import makes the original filter form compile. So this is name capture by POOL MEMBERSHIP, not by the import list: DESIGN #6985 Class B in its inverting direction, and silent -- nothing reports that filter rebound. Refuted by execution before landing anything, recorded in the carrier so nobody repeats them: trailing comma in the parameter list; filter over an imported record type; List not imported; two filter calls over different element types in one module; a lambda or parameter name colliding with an imported symbol; cross-call-site unification. The import cannot be dropped -- ModuleStorageBinding, ParsedFromSource and ProducedByBehavior are all genuinely used -- so the only local repair routes around the capture. Per DESIGN section 5 that is a WORKAROUND and may only land as a DECLARED SCAFFOLD, never a quiet respelling: the body selects with flat_map (the spelling the sibling function already uses over the same type with the same binding), and commit_writer_index_filter_capture_scaffold states the root cause, the discriminating measurement, the refuted hypotheses, why the import cannot be dropped, and the dissolve-on -- the namespace-only resolution lane, after which the filter spelling is restored and the row deleted. Behaviour is unchanged: it selects exactly the entries whose path matches. commit_writer_admission_witness_test 13/13 exercises admit_commit_writer, which calls this function, so that is verified by execution and not merely by compiling. This is NOT this branch's defect. It is fixed here because it reds main itself and every job on this PR, and main landed #7608 while its own CI was still queued. Also: two more files from main's #7623 import ContentHash from std.types. Same manifest_content_identity field as the two prior batches, so same grounding onto Fnv1a64Structural, with the contract fixture becoming a valid 16-hex digest. Whole-tree compile 0 blocking errors; regen_divergence_count=0; heal no drift; commit_writer_admission 13/13; live_rust_observation_contract 12/12. --------- Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.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>
First slice of the actuation bundle. No host mutation. This lands the model and the wall a later mutating step must pass through. Stacked on #7595 (
runner_activation,build_cache_instance); the diff here is two new files.The defect
srv4, observed 2026-08-01: no listener, no process, no unit, a
server.sockon disk, and an sccache error log whose entire contents areAddress in use (os error 98). A server born inside a runner cgroup died with that cgroup without unlinking its socket, so every later server fails to bind. The host has been uncached since roughly 2026-07-26 for that reason alone.BuildCacheEndpointObservation.EndpointAbsentcannot express this. It answers is anybody listening — the right question for placement, the wrong one for actuation. Under it, srv4 and a host that simply has no cache yet are the same state. They are not: one needs a unit started, the other needs a file removed first and then a unit started, and starting the unit without removing the file reproduces error 98 forever. A state-space conflation whose cost is measured rather than hypothetical.So
EndpointPathStateis a separate axis from the owner — absent / listening / stale socket / path conflict / unavailable.EndpointStaleSocketandEndpointPathAbsentboth project toEndpointAbsenton the ownership axis, which is exactly the distinction that axis is not built to carry.Why the unlink is admitted rather than an
ExecStartPreThe obvious fix is
ExecStartPre=/bin/rm -f server.sock, and it is an absorbing fallback — it unlinks in every path state rather than the one where unlinking is correct:rmdoesEndpointPathAbsentEndpointPathConflictEndpointListeningThe listening case leaves a running process no client can reach and that will not be replaced, because it still holds memory and still parents compiles. Silent in exactly the way the stale socket was silent, so the remedy recreates the disease one layer up. And an unconditional
rmnever reports having removed something it should not have, so the deficit's frequency is zeroed by construction.admit_stale_socket_unlinkadmits only fromEndpointStaleSocketunderObservationCurrent; every other state is a positively named, located refusal. The unlink is a separate admitted step before the unit start, able to refuse independently of it — never a line inside the unit.Three shapes deliberately avoided
review 46217found in the owner context. The four facts an observer must establish are stated as its contract, not re-represented as data.ObservationCurrencyUnknownrefuses exactly asObservationStaledoes. Not knowing whether a read is current is the same evidence as knowing it is old. An arm that graded them would fire precisely when/procis least readable — which is when a bind race is likeliest.Test plan
Green by execution — 8 controls, 0 failures:
unconditional_unlink_sketchis the RED control —ExecStartPre=rm -fwritten as a predicate, answeringtruein all five states. Two witnesses assert the admission differs from it on the two destructive states rather than merely asserting the admission refuses: a refusal where the naive fix would also have been harmless proves nothing about the wall's worth. Per DESIGN §4b the control stays enrolled rather than retiring once the wall lands.Honest residual
.daghas no visibility modifiers, so a caller can hand-writeEndpointStaleSocketwith no observation behind it — an explicit lie in the source rather than a forgotten check, the same residualBuildCacheInstanceReadycarries. It closes when node/subtree visibility grants make the constructor private to its observer.No observer producer ships here. That is deliberate: the observer is a mutating-adjacent host read, and the operator's sequencing holds host work until the generated units, this state machine, captured-base adoption, rollback, and the controlled-load protocol are reviewed together.