Skip to content

fix(marketplace): serialize registry writes to prevent lost updates - #473

Merged
christso merged 2 commits into
EntityProcess:mainfrom
Vic-Wang-WTG:fix/marketplace-save-race
Sep 14, 2026
Merged

christso merged 2 commits into
EntityProcess:mainfrom
Vic-Wang-WTG:fix/marketplace-save-race

Conversation

@Vic-Wang-WTG

Copy link
Copy Markdown
Contributor

Fixes #472

Summary

  • validateAllPlugins validates every configured plugin in parallel (Promise.all). Each plugin backed by a remote marketplace independently calls updateMarketplace(), which loads the shared marketplaces.json, mutates its own in-memory copy, and saves — with no coordination between concurrent callers.
  • Two calls racing on the same file let whichever saves last silently discard the other's already-persisted change (a lost update, reproducible on any OS), and on Windows the concurrent temp-file renames onto the same destination can also throw EPERM, failing allagents update outright (see fix(sync): concurrent plugin validation races on marketplaces.json, losing updates and throwing EPERM on Windows #472 for the traced repro).
  • Adds an in-process, per-registry-path async lock (withRegistryLock) and routes every load-mutate-save sequence through it: updateMarketplace's final save, refreshMarketplace, and removeInvalidMarketplaceRegistration. Each writer now reloads the latest saved state immediately before merging its own change in, instead of overwriting with a stale snapshot loaded before the queue.
  • This only serializes writers within one process — it is not a cross-process file lock (out of scope for this fix; no other part of the tool needs one today).

Test plan

  • New test tests/unit/core/marketplace-update-concurrency.test.ts: two concurrent updateMarketplace() calls updating two different marketplaces on the same shared registry file — confirmed it fails on the pre-fix code (silently drops one update) and passes with the fix.
  • bun test — full suite: no new failures (7 pre-existing failures in tests/unit/core/native/types.test.ts, unrelated to this change, present on origin/main before this branch too).
  • bun run typecheck — clean.
  • bun run lint — clean.
  • Built the CLI and ran allagents update three times against the real workspace that originally reproduced the EPERM crash (4 marketplaces across user + project scope) — all three runs now exit 0.

validateAllPlugins validates every configured plugin in parallel. Each
plugin backed by a remote marketplace independently calls
updateMarketplace(), which loads the shared marketplaces.json, mutates
its own in-memory copy, and saves — with no coordination between
concurrent callers.

Two calls racing on the same file let the one that saves last silently
overwrite the other's already-persisted change (a lost update), and on
Windows the concurrent temp-file renames onto the same destination can
also throw EPERM, failing `allagents update` outright.

Add an in-process, per-registry-path async lock and route every
load-mutate-save sequence through it (updateMarketplace's final save,
refreshMarketplace, removeInvalidMarketplaceRegistration). Each writer
now reloads the latest saved state before merging its own change in,
instead of overwriting with a stale snapshot loaded before the queue.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-14T01:19:47.920315Z 0bd27f8 PR opened
🔒 Security Review Completed 2026-09-14T01:20:38.707303Z 0bd27f8 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@christso

christso commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

I pushed 334215d to complete the registry locking fix.

What changed

The original PR queued selected registry saves. This update makes the queue the shared registry transaction boundary:

  • mutateRegistry() loads the latest registry after queue acquisition, applies one mutation, and saves only when state changed.
  • addMarketplace, both removeMarketplace scopes, updateMarketplace, invalid-registration cleanup, and refresh commits now use that boundary.
  • Failed, local, missing, and invalid marketplace updates are no longer written back from stale snapshots.
  • Update commits validate the current name, source, and path, then change only lastUpdated. Removed or replaced registrations are not resurrected or overwritten.
  • A concurrent newer lastUpdated value is preserved instead of moving backward.
  • Refresh conflict recovery remains inside the registry queue so another registration cannot commit during stale cache cleanup or restoration.
  • Idle queue tails are removed without allowing an older completion to delete a newer queued tail.
  • Remote add checks registry readability before cloning, then reloads the authoritative state inside the transaction.

Coverage

The original 30 ms race now uses explicit promise barriers. Regressions cover:

  1. two marketplace updates finishing out of order;
  2. a failed update-all entry racing a successful named update;
  3. removal while a named update is in flight;
  4. a newer same-marketplace timestamp winning over an older update-all result;
  5. concurrent marketplace registrations preserving both entries;
  6. unreadable registry data preventing clone or cache creation.

Verification

  • focused marketplace unit suite: 98 passed, 0 failed
  • targeted concurrency suite: 31 passed, 0 failed
  • bun run typecheck: clean
  • bunx biome lint src/core/marketplace.ts: clean
  • bun run build: clean
  • bun test tests/e2e/plugin-update.test.ts: 3 passed, 0 failed
  • built CLI against a temporary workspace with two real local Git remotes: exit 0; both registry timestamps and cache HEADs updated

The contract remains in-process only. This does not add an OS/file lock or cross-process coordination.

@christso
christso merged commit dc172f8 into EntityProcess:main Sep 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(sync): concurrent plugin validation races on marketplaces.json, losing updates and throwing EPERM on Windows

2 participants