fix(runtime): put the fork path in the bucket id, not just in the counter - #105
Merged
Merged
Conversation
…nter Numbering fork sequences per path stopped a cascade in which one extra spawn early in a request renumbered every fork after it. It also removed a property the per-correlation counter had given for free, because the bucket id was still composed from the sequence alone and every path's sequence starts at one. Two different fork sites under the same parent therefore both minted root::fork-1 and collided. A bucket is the scorer's statement that a region of work is unordered. unordered_distinct_lineage excuses a value divergence between two events whose buckets differ, and reads a shared bucket as a claim that the two are ordered with respect to each other. So a collision means a genuine race between two detached tasks stops being excused and arrives as a divergence — the same class of false report the per-path change was made to prevent, reintroduced from the other side. Put the path in the id. The sequence stays local to the callsite, which is what keeps a fork elsewhere in the request from renumbering this one, and the id now also carries which callsite it came from, which is what keeps two callsites apart. The key is the stable callsite hash already used for addresses: FNV-1a over the path, deterministic across runs and toolchains, so a recording and its candidate compute the same value where a DefaultHasher would not. Nothing parses a bucket id. The only reader outside the layer is is_fork_region, which tests for the ::fork- marker and is unaffected; its doc comment named the old shape and now says the shape belongs to the layer. The test that comes with this fails on the unmodified parent for the right reason, reporting root::fork-1 on both sides. The two tests written with the per-path change pass throughout, on the parent and here, because each asserts a property that change added rather than one it inherited; that is why the collision reached main behind a green suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PYTPJrzSTR1CWbAjouucTs
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.
Numbering fork sequences per path, in #100, stopped a cascade in which one extra spawn early in a request renumbered every fork after it and re-addressed everything beneath them. That part was right and stands. What went with it was a property the per-correlation counter had been giving for free, because the bucket id was still composed from the sequence alone:
Every path's sequence starts at one, so two different fork sites under the same parent both minted
root::fork-1. Measured across that change:What the collision costs
A bucket is the scorer's statement that a region of work is unordered.
unordered_distinct_lineageexcuses a value divergence between two events whose buckets differ, and reads a shared bucket as a claim that the two are ordered with respect to each other. So a collision stops a genuine race between two detached tasks from being excused, and it arrives as a divergence — the same class of false report per-path numbering exists to prevent, reached from the other direction. It is latent in hyperswitch today, where the two fork sites sit on different flows.The change
Put the path in the id as well as in the counter. The sequence stays local to the callsite, which is what keeps a fork elsewhere in the request from renumbering this one; the id now also carries which callsite it came from, which is what keeps two callsites apart.
The key is
stable_callsite_hash, the FNV-1a already used for callsite addresses: deterministic across runs and across toolchains, which aDefaultHasheris not, so a recording and its candidate compute the same value.task_idis composed the same way for consistency.Nothing parses a bucket id. Checked workspace-wide: every
::fork-occurrence outside the composition site is either a.contains()or a test literal that needs only the marker. The one reader outside the layer isis_fork_region; its doc comment named the old shape and now says the shape belongs to the layer.Verification
just verifyexits 0 unpiped on 1.96.0 — 55 test blocks, zero failures. MSRVcargo +1.85.0 check -p deja-runtime -p deja-context --all-targetsexits 0.The new test fails on the unmodified parent, for the right reason, so this carries its own proof rather than an argument:
That mutation is the discriminating one: it breaks only the new test, so the test is about the path key specifically and not about forking in general.
Why #100 was green
Both tests written with #100 assert properties that change added — a fork elsewhere must not renumber this one, and counters are evicted when the owning span closes. Neither asserts the property it inherited: that two sibling forks are distinct. The lost guarantee was not in the deleted lines, so neither the diff nor the mutation matrix showed it — every mutation run there broke a property that had already been written down. Worth carrying forward when a keying or numbering scheme changes: the properties the old scheme guaranteed incidentally are the ones nothing asserts.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PYTPJrzSTR1CWbAjouucTs