Repository navigation
Never file a kernel's changed-identity report as a gate verdict (graph amendment 29) - #1140
Merged
Merged
Conversation
…dment 29) run_graph derives node keys before any node runs. gates.battery@1 raised when its binding registry or a binding's source closure changed after the kernel was built, and the executor filed that exception as a `fail` verdict under the unedited registry's key, so later runs of the correct registry on the same store were served the stale `fail` (#1125 review, finding 1). - errors.py: KernelIdentityChangedError, a NodeRejectedError, exported from microcosm.graph. - executor.py: re-raise it for any role instead of filing it; the run refuses, nothing for the node is stored, earlier work is kept. - gates.battery@1: raise it from implementation_hash and from run, which now checks before evaluating, before returning and when evaluation raises; an unreadable or unparseable closure file is a change too. - The executor deliberately does not re-derive hashes itself: most gate hashes re-read module files, and a rewrite under code already imported would refuse correct builds. - Amendment 28, transport-graph-kernels Gates section, changelog. - Tests: graph-level contract tests and a Hypothesis same-store vs cold-store differential; the review's three-run probe and closure-file changes through the real battery. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refresh against main ecf87ee for a CI run on the pinned ubuntu-24.04 image. The merge is clean. Amendment 29 now says when each key-movement check ran: 41 implementation_hash definitions on 2026-10-07, and the 71 on main on 2026-11-11. Neither check found another kernel whose hash reads a file this change edits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the independent review of 704caec (APPROVE_WITH_NITS): - The executor's re-raise of KernelIdentityChangedError now adds a note naming the node and kernel, keeping the error's type, message and cause. The reviewer's probe test asserts the note. - docs/transport-graph-kernels.md said a change after key derivation "is refused at the gate node". A gate served from the store does not run, so nothing is refused there; the record it serves was filed under that key by a run whose checks passed. The paragraph now says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Handing off to the Microcosm hub. Head: Reviews (in-session Opus 5.5; no Subfleet lane was dispatchable):
CI at
Depends on: nothing. Decision: none. Follow-ups, none blocking:
The evidence is in |
Contributor
Author
|
I merged this at 62fa3f7. All 12 checks passed on that head, and two independent Opus 5.5 reviews approved it, the second at this head. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A kernel that reports a changed identity no longer has that report filed as a gate verdict (graph amendment 29). This closes finding 1 of the independent review of #1125 (Subfleet job 20261007-081708).
The defect
run_graphderives every node key fromkernel.implementation_hash()before any node runs.gates.battery@1raises inrun()once its binding registry, or a file in a binding's source closure, changed after the kernel was built. The executor turned every gate-kernel exception into afailverdict (_failed_gate_result, amendment 7) and stored it under the already-derived key of the unedited registry. A later run with the correct registry on the same store was then served thatfailas a cache hit.Reproduced on
origin/main(9f0a2b9): the binding was edited from the population observer aftertoy.calibrate, then the toy graph was run throughtoy.gates.terminal:The fix
microcosm.graph.KernelIdentityChangedError, aNodeRejectedError, is added toerrors.pyand exported. A kernel raises it when state itsimplementation_hashdescribes has moved since that hash was fixed.except KernelIdentityChangedErrorcomes before the generic handler, for any role (executor.py:2903). It adds a note naming the node and kernel, then re-raises. For a gate this replaces amendment 7's verdict, and F4's "becomesfail", in that one case. The run refuses and nothing for the node is applied or stored. The existing_settle_failed_runkeeps what the run had already completed, because sources are unchanged and those keys still name what computed them. The next run reuses that work and computes the gate afresh.gates.battery@1. It raises the new type, which used to beValueError, fromimplementation_hash()and fromrun().run()now checks before it evaluates, before it returns, and when evaluation raises, chained to that exception. A closure file that can no longer be read or parsed counts as a change: before, it surfaced as a rawSyntaxErrororFileNotFoundErrorand was filed as afail. The message now names the source closure, which was Add shared transport-graph kernels: targets, ordered calibration, gates, diagnostics and export (NZ v0, G5b) #1125 review nit 4.docs/graph-acceptance.mdgains amendment 29, which is runtime-only:decl.pyandkernel.pyare untouched and the interface lock is unchanged.docs/transport-graph-kernels.md(Gates) is updated, and there is a changelog fragment.Why the executor does not re-check hashes itself
The first draft of this branch also re-derived every gate's
implementation_hashright afterrun()and refused on any change. The internal review executed a probe against it, and that version is not what ships.Seven of the eight production gate kernels hash module files from disk on every call through
source_hash: the UK gates, both export readbacks andgeography.gate@1. A file rewritten under a running process does not change the code it already imported. That draft would therefore have aborted a correct UK build at its first gate after any save, checkout or pull that touched one of those files, at any point after key derivation.So the check belongs to the kernel, which knows whether its identity is in-memory state.
test_a_hash_that_moves_without_a_report_is_not_second_guessedpins that gates whose hash only re-reads files keep their current behaviour.Invariants
Each is scoped to what the tests execute.
resume="auto"and"forbid", for every behaviour (pass, fail, raise), every move (none, before the gate, inside its run) and any identity value, including one equal to the original. The Hypothesis property with a same-store vs cold-store differential istest_graph_executor_gate_identity.py:523.KernelIdentityChangedErrorif and only if the kernel reports a change. Otherwise its gate outcome is the unperturbed kernel's, and amendment 7 is untouched for every other exception (:426, and the property).resume="require"then raisesStoreMiss(:349, under auto and forbid).:396).test_transport_graph.py:257,:355,:415). After each refusal, a fresh registry derives the original key and computespassas a miss, every other node hits, and a cold store agrees.Not covered. A move undone before the kernel's next check is not seen. Only
gates.battery@1checks its own identity. On mainecf87ee24, eight otherimplementation_hashdefinitions read attributes set at construction. There were seven at the October base; main'sSimulateSolveZeroKerneladded the second inrules_kernels.py.uk_runtime/graph_kernels.pyread transforms and stages;_engineinus_runtime/graph_kernels.py(two) andframe/kernels.py(one);_adapter_classesinframe/rules_kernels.py(two).None of them is a gate, and none is re-checked, as before. Amendment 29 asks any kernel whose identity can move in process to check it and raise the new error.
Key movement
Executed by deriving node keys at 9f0a2b9 with and without this change, without running the graph, before rebasing onto #888. The rebase left this PR's hunks unchanged apart from one merged import line in
executor.py.gates.battery@1, whose own source changed, andexport.prepare@1andtransport.package@1, which hash the battery's source. On the toy transport graph the gate nodes andtoy.export.preparemove; every other node keeps its key.implementation_hashinpackages/*/srcreads an edited file. All 41 definitions were checked on 2026-10-07. On 2026-11-11 the 71 definitions on mainecf87ee24were checked again, by reading everysource_hashargument list and the module lists behindevidence_implementation_hash, the income-evidence hashes andtransport_compile_modules(). Onlygates.battery@1andterminal_kernels.py's two kernels name an edited module. The eight gate kernels are the same eight as at the base.failstored by the old code sits under a key this version never derives, so it is never served.Red, green and mutants
executor.pyfrom 9f0a2b9 (everything else at head), 12 of the new or changed tests in these files fail, including every closure-change case. All pass at head.resume="forbid"repair claim, and the F4 and amendment 7 carve-out.Refresh on 2026-11-11
Head
704caec97merges mainecf87ee24(clean, no conflicts) so CI runs on the pinned ubuntu-24.04 image. Main changed none of this PR's executor, errors or__init__lines, so the cited lineexecutor.py:2903holds. Main'sgate_kernels.pygainedgraph_evidenceand new evidence decoders (#1202, receipt v2 and release-input evidence). Those land inside_evaluate, so they sit behind the identity checks. The merge commit also rescopes amendment 29's key-movement sentence to say when each check ran. A disk pause is on, so the targeted files below were not rerun locally at this head. CI runs them.Head
62fa3f7cdthen addresses nits 3 and 4 of the review of704caec97(below): the executor's re-raise adds a note naming the refusing node and kernel, and docs/transport-graph-kernels.md says that a gate served from the store does not run.test_graph_executor_gate_identity.pypasses locally at this head (7 passed).Review
An in-session Opus 5.5 review of
704caec97returned APPROVE_WITH_NITS (~/reviews/microcosm-gate-identity-2026-10-07/runs/review-1111/review-out.md). It ran three targeted files, 62 passed. Swapping in baseexecutor.pyturned 12 tests red, and all five battery mutants were killed. Its nits 3, 4 and 6 are fixed: nit 6 is the kernel count under "Not covered". It also found that the shared.venvlacksmicrocosm-emitter-interface0.14.0, which main now needs. Two of its nits stay as they are. Nit 5: an unreadable closure file is defined as a change. Nit 7: the(path, mtime_ns, size)digest cache predates this PR. A delta review of62fa3f7cd(the nit-fix commit) also returned APPROVE_WITH_NITS (~/reviews/microcosm-gate-identity-2026-10-07/runs/review-1111-delta/review-out.md). It checked the following:add_noteturns exactly the new assertion red (1 failed, 6 passed);kernel.run;9f0a2b922.Its one remaining nit is optional. The docs sentence "filed under that key by a run whose checks passed" is loose for a battery run whose
require_paramsorrequire_outputsraised before the first identity check. The executor files thatfail, and it is correct for that key, since it depends only on keyed params and class constants.Tests run locally (at
7d6ae12c5, before the refresh)Targeted files only, one at a time, with
-p no:cacheprovider --basetemp=.hub-scratch/pytest -o tmp_path_retention_policy=failed. The environment is the shared.venvwith this worktree's sources first on the path (hypothesis 6.167.1andjsonschema 4.26.0added from the lock into scratch). Whole suites go to CI.test_graph_executor_gate_identity.py(new)test_graph_executor.pytest_acceptance_f_gates.pytest_graph_explain.pytest_graph_interface_lock.pytest_graph_parity_pins.pytest_graph_executor_source_identity.pytest_graph_acceptance_burndown.pytest_graph_manifest.pytest_transport_graph.pytest_transport_gate_kernels.pytest_transport_terminal_kernels.pytest_transport_binding_identity.pytest_transport_target_kernels.pytest_ordered_kernels.pytest_nz_spec_package.py465 passed, 0 failed. Not run locally: the full suites, the US and UK engine jobs, and a wheel build.
Also run:
ruff checkandruff format --checkon the changed Python files (ruff 0.15.16, clean);tools/ci_test_plan.py verify(ok);tools/graph_acceptance_burndown.py --verify(ok). The acceptance suite files (test_acceptance_*.py) are not edited.Owners
executor.py. Max's sessions wrote 47 of its 50 commits. María Juaristi wrote one (507e2bd, the amendment renumbering and interface re-lock), and Anthony Volk wrote two, both in Export Microcosm schema metadata for Orrery #888 (52e5545 and e6279af, the Orrery export expansion validation).gate_kernels.py. Added by the NZ hub's sessions in Add shared transport-graph kernels: targets, ordered calibration, gates, diagnostics and export (NZ v0, G5b) #1125 (e84ba89, 90503f4).decl.pyandkernel.pyare untouched.This PR adds no executor line above line 2903, so nothing it cites moves. Separately,
packages/microcosm-build/src/microcosm/build/nz/benefit_unit_rule.jsoncitesexecutor.pylines 636-643 and 1550-1573. #888 has already moved those functions to 638 and 1531 onmain; I've raised that as a separate task.axiom: n/a: graph executor runtime contract, no policy
🤖 Generated with Claude Code