Note — AI-generated analysis. This issue was produced by an automated AI code analysis (Claude Code) reading this repository at commit d4de5a1. Treat the claims and code pointers below as a starting point and review them carefully against the current source before acting — they may be incomplete or wrong.
Summary
Group-repo putRecord does not support a swapRecord (optimistic-concurrency) precondition, so concurrent writes to the same record — e.g. two admins editing an org profile at once — silently last-writer-wins instead of returning a conflict.
Current behavior
There is no swapRecord, swapCommit, or InvalidSwap handling anywhere in the service (empty across src/, lexicons/, tests/, docs/). Specifically:
- The
app.certified.group.repo.putRecord input lexicon declares only repo, collection, rkey, record — no swapRecord/swapCommit (lexicons/app/certified/group/repo/putRecord.json).
- The handler casts the body to exactly
{ repo, collection, rkey, record } and forwards that; it neither declares nor explicitly forwards a swap precondition (src/api/repo/putRecord.ts).
So a caller cannot express "only write if the current record is still CID X," and a CID mismatch is never detected.
Impact
Concurrent multi-writer edits to a group repo silently overwrite each other (last-writer-wins). This is most visible for the org/group app.bsky.actor.profile record, where two admins editing settings concurrently lose one set of changes with no error.
What's already in place (so the fix is small)
The error-relay half already works: proxyToPds forwards PDS 4xx errors to the caller preserving the error discriminator (throw new XRPCError(err.status, err.message, err.error) in src/api/util.ts). So if a swapRecord precondition were forwarded to the group's PDS and it returned InvalidSwap (400), the service would already relay it as a 400 carrying InvalidSwap.
Proposed change
- Add
swapRecord (and ideally swapCommit) to the app.certified.group.repo.putRecord input lexicon (mirroring com.atproto.repo.putRecord).
- Forward the precondition explicitly from the handler to the PDS agent in
src/api/repo/putRecord.ts.
No change needed to error handling — the InvalidSwap 400 relay already exists via proxyToPds. (Consider the same for repo.deleteRecord if delete-with-swap is desired.)
Context
Surfaced while answering a downstream contract question from certified-app (hypercerts-org/certified-app#112, tracker judgment-006). The app side already handles the atproto InvalidSwap discriminator and forwards swapRecord on the group profile path, but it's a no-op until the service honors it. Until this lands, group-repo putRecord should be treated as last-writer-wins.
Pointers: lexicons/app/certified/group/repo/putRecord.json, src/api/repo/putRecord.ts, src/api/util.ts (proxyToPds). Analysis only — no changes made.
Summary
Group-repo
putRecorddoes not support aswapRecord(optimistic-concurrency) precondition, so concurrent writes to the same record — e.g. two admins editing an org profile at once — silently last-writer-wins instead of returning a conflict.Current behavior
There is no
swapRecord,swapCommit, orInvalidSwaphandling anywhere in the service (empty acrosssrc/,lexicons/,tests/,docs/). Specifically:app.certified.group.repo.putRecordinput lexicon declares onlyrepo,collection,rkey,record— noswapRecord/swapCommit(lexicons/app/certified/group/repo/putRecord.json).{ repo, collection, rkey, record }and forwards that; it neither declares nor explicitly forwards a swap precondition (src/api/repo/putRecord.ts).So a caller cannot express "only write if the current record is still CID X," and a CID mismatch is never detected.
Impact
Concurrent multi-writer edits to a group repo silently overwrite each other (last-writer-wins). This is most visible for the org/group
app.bsky.actor.profilerecord, where two admins editing settings concurrently lose one set of changes with no error.What's already in place (so the fix is small)
The error-relay half already works:
proxyToPdsforwards PDS 4xx errors to the caller preserving the error discriminator (throw new XRPCError(err.status, err.message, err.error)insrc/api/util.ts). So if aswapRecordprecondition were forwarded to the group's PDS and it returnedInvalidSwap(400), the service would already relay it as a 400 carryingInvalidSwap.Proposed change
swapRecord(and ideallyswapCommit) to theapp.certified.group.repo.putRecordinput lexicon (mirroringcom.atproto.repo.putRecord).src/api/repo/putRecord.ts.No change needed to error handling — the
InvalidSwap400 relay already exists viaproxyToPds. (Consider the same forrepo.deleteRecordif delete-with-swap is desired.)Context
Surfaced while answering a downstream contract question from
certified-app(hypercerts-org/certified-app#112, tracker judgment-006). The app side already handles the atprotoInvalidSwapdiscriminator and forwardsswapRecordon the group profile path, but it's a no-op until the service honors it. Until this lands, group-repoputRecordshould be treated as last-writer-wins.Pointers:
lexicons/app/certified/group/repo/putRecord.json,src/api/repo/putRecord.ts,src/api/util.ts(proxyToPds). Analysis only — no changes made.