Repository navigation
feat(store)!: a version named on a put must be the next one - #327
Merged
Merged
Conversation
A caller that reads an entity, decides from what it read and then writes a new version can't tell whether another writer got there first: versions only go up, so its write lands on top and buries the other one. VersionedEntityStore.put, A2AAgentStore.put_document and A2AAgent.put take expected_version (0 for an id with no versions). The put then lands on exactly expected_version + 1, with no retry, and the (id, version) unique index refuses it with ConcurrentModificationError if another write took that version first. Without the argument the put is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Replaces expected_version with the version an entity already carries. A put that names a version is stored as exactly that one, which must be the id's next (latest + 1): taken, the latest itself or past the next, it raises ConcurrentModificationError and writes nothing, as when another write takes it first. A put that names none takes the next free version, as before. Agents, tasks, evals, task steps and envs ignored a named version until now; artifacts used it as a starting point and retried at the next free one, so a document could land on a number its objects weren't stored under. The artifact helpers whose number only reaches the document now name none and are allocated one, keeping their race behavior; the ones that bake it into an object key or image tag keep naming it. FileArtifact.put_at checks the id before uploading, through ArtifactStore.check_writable, now that it no longer reserves a version. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…r takes their version They key the object by a version they reserve and wrote that version on the document too, so a writer publishing it during the upload failed the put under the strict rule. The document now takes the next free version, as the Docker image helpers' already does: the same one unless a concurrent write took it first. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Collaborator
Author
Chaos runSetup: a throwaway Linux VM running a 3-node MongoDB 7 replica set in Docker with 2 s elections, driven through Workload: writer processes on 2–3 ids, mixing four kinds of
A put that failed with a transport error was retried with the same document. Faults:
Checked after each round:
Two notes, both about puts that name no version, which this PR doesn't change:
🤖 Generated with Claude Code |
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.
Why
A caller that reads an entity, decides from what it read, and then writes a new version can't tell whether another writer got there first. A
versionpassed to a put doesn't help today, even though it reads like a guard:What
A version named on a put is stored as exactly that version, and it must be the id's next one (latest + 1). Anything else raises
ConcurrentModificationErrorand writes nothing.None, or0on an artifact)ConcurrentModificationErrorif another write took it firstConcurrentModificationErrorConcurrentModificationErrorVersionedEntityStore.insert_next, whichVersionedEntityStore.putandArtifactStore.put_documentboth use. It reads the latest version, then inserts. The(id, version)unique index settles two writers that both read k.ConcurrentModificationError, the oneupdate_metadataraises on a compare-and-set miss.Artifact helpers
Every helper that reserved a number with
next_versionand wrote its document with it now leaves the document's version to the store. A concurrent writer moves the document to the next free version, as before, instead of failing it.DockerImageArtifact.put_tar,put_refandput_context;FileArtifact.put_atandput_existing; the skill, CLI and file-artifact-universe puts; and the snapshot and collected-artifact registrations. They no longer reserve one.FileArtifact.putandput_bytes(object key), andDockerImageArtifact.putandput_from_github(:vNtag). They still reserve it for the key or tag, while the document takes the next free version. That's the same number unless a concurrent write took it.ArtifactStore.check_writable(id)keeps the id checknext_versionmade before an upload.FileArtifact.put_atcalls it, since it no longer reserves a version.So no helper in this repository names a version. The rule applies only to versions a caller passes.
Breaking
A caller that passed a version other than the next one used to get the next free version. Now it gets
ConcurrentModificationError. Nothing in this repository does that.plugin-apireports no break to the signatures; the!marks the behavior change.Tests
Conformance suite, which every document-store backend runs:
versioned_entity_store_put_at_a_version: the next version lands. Taken, latest and past-the-next versions write nothing, and an unnamed put allocates.racing_puts_at_one_version_have_exactly_one_winner: eight writers name the next version. One lands and seven are refused.test_store_put_document.py, rewritten for the rule:It also covers a
FileArtifact.put_byteswhose version another writer publishes during the upload, which is still published.put_version_test.py:A2AAgent.putwith versions.Full unit tier: 10,402 passed, 63 skipped. ruff is clean, and
plugin-apireports no break.Docs
The docs should say how
versionbehaves on a put where they describe registering artifacts and A2A agents.🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge; the file helpers now preserve automatic retries.Summary
Caller-supplied versions must now be the next version. Automatic writes still allocate a free version and retry collisions.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A[Write artifact document] --> B{Version supplied?} B -->|Yes| C[Require the next version] C --> D{Insert succeeds?} D -->|No| E[Raise ConcurrentModificationError] B -->|No| F[Allocate a free version] F --> G{Insert succeeds?} G -->|Collision| F G -->|Yes| H[Return saved version and object_url] D -->|Yes| HReviews (3) · Last reviewed commit: "fix(store): FileArtifact.put and put_byt..." · Reviewed by Greptile