fix(world): normalize vector clocks and enforce phase-105 conflict check (#275) - #360
Merged
Darkvader-ship-it merged 1 commit intoSep 27, 2026
Conversation
…eck on POST /api/world (PHASE-STELLAR#275) checkWorldConflict was imported by POST /api/world but never called, so concurrent world edits still overwrote each other. Worlds now carry a per-author vector_clock; clocks arriving as a Map, plain object, or [node, counter] entries array are normalized before comparison so equal clocks no longer register as divergent. Stale or concurrent saves return 409 WORLD_VERSION_CONFLICT with the server version, clock, and order.
|
@dannyy2000 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
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.
Closes #275
Closes #272,
Closes #273,
Closes #274.
These share the same evidence block but point at different files (
app/api/x402/verify/route.ts,lib/wash-trading.ts,lib/faucet-deny-list.ts). This PR does not change those files, so they should stay open.Problem (#275)
POST /api/worldimportedcheckWorldConflictbut never called it.expected_versionwas accepted and then ignored, so concurrent co-author saves still overwrote each other even withphase-105enabled.Map, or as a[node, counter]entries array (JSON.stringify([...map])). Compared as-is, these shapes never match, so two equal clocks look divergent.Changes
lib/world-conflict.tsnormalizeVectorClock()turns aMap, a plain object or an entries array into one canonicalRecord<string, number>. It drops zero counters, keeps the highest value when a node appears twice, and returnsnullfor malformed input.compareVectorClocks()returnsequal,before,afterorconcurrent.incrementVectorClock()works on stored clocks in any of those shapes.checkWorldConflict()accepts an optionalexpectedVectorClockand returnsorderandserverVectorClock. It still checksversionwhen the clocks agree, so worlds saved before this change keep working.lib/narrative-world-store.ts:saveWorldForCollectionnow bumpsvector_clock[creator_wallet ?? "anonymous"]and returns the saved record.app/api/world/route.tsexpected_version(non-negative integer) andexpected_vector_clock, returning400if either is invalid.409 WORLD_VERSION_CONFLICTwithorder,server_version,client_version,server_vector_clockandcurrent.versionandvector_clock.GETexposesvector_clockfor each world.phase-105rows toPROJECT_ARCHITECTURE.mdanddocs/TECHNICAL.md.Behavior
expected_versionnorexpected_vector_clock: unchanged, no check.Tests
npx tsx --test lib/__tests__/world-conflict.test.ts: 15/15 pass.before) and diverged (concurrent) conflicts; matching clocks with different shapes; and the version fallback for legacy worlds.lib/__tests__/narrative-search.test.tsstill passes.Known limitation
The check and the write are still a read-then-write on the JSON store, so two requests that interleave exactly between those steps can both succeed. Closing that gap needs an atomic compare-and-swap in the store, which is outside the scope of this PR.