Skip to content

Retry concurrent capability adds on the latest snapshot (CL-7216) - #600

Merged
TheGreatAxios merged 6 commits into
mainfrom
cl-7216-concurrent-agent-directory-capability-edits-silently-clobber
Sep 4, 2026
Merged

TheGreatAxios merged 6 commits into
mainfrom
cl-7216-concurrent-agent-directory-capability-edits-silently-clobber

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

POST /:definitionId/capabilities was a lock-free read-modify-write of the definition asset. Two overlapping adds against the same definition could snapshot the same tree; the later write silently dropped the earlier pin (last-write-wins).

The add now retries the loser against the latest snapshot. A per-asset lock makes the stale-snapshot check atomic so the losing edit reapplies on the winner tree instead of clobbering it. Both capability-adds land.

The same helper is used by the workflow-run capability route, which had the identical RMW.

Test

two concurrent capability-adds on the same definition both land fires two overlapping POSTs of different tool packages on one definitionId and asserts the stored definition carries both pins.

Out of scope

CL-7261 batched / multi-select UI (canceled).

Linear: CL-7216

Fixes CL-7216

Two overlapping POSTs against the same definition must both land their
pins. The suite uses a live in-memory asset so a last-write-wins clobber
is observable.
Capability-add was a lock-free read-modify-write of the definition
asset, so two overlapping POSTs could each snapshot the same tree and
the later write would drop the earlier pin. The add now re-reads under
a per-asset lock and reapplies the losing edit on the winner tree
instead of last-write-wins.
PUT instructions, PUT skills, and DELETE model still last-write-wins
clobber a concurrent edit of the same asset. The suite races those
routes so a lost prompt, skill pin, or model-clear is observable.
PUT instructions, PUT skills, and DELETE model were still lock-free
read-modify-writes of the same asset, so a concurrent edit could
snapshot the old tree and clobber the other field. They now share the
capability-add per-asset lock and reapply on the latest snapshot.

Hub is a single replica; the lock is an in-process chain, not a
distributed advisory lock.
POST pin was still a lock-free read-modify-write of the same asset, so
a concurrent model-clear could last-write-wins drop the pin (or the
pin could restore the model). The suite races those two routes so a
lost skill or model-clear is observable.
POST /pin was still a lock-free read-modify-write of the definition
asset, so a concurrent DELETE model could snapshot the old tree and
clobber the pin (or the pin could restore the model). It now shares
the capability-add per-asset lock and reapplies on the latest snapshot.
@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Fixes CL-7216

@TheGreatAxios

Copy link
Copy Markdown
Contributor Author

Self-review

Verdict: the retry-on-latest-snapshot fix is correct — the per-asset promise-chain lock makes the stale-snapshot check atomic, so a losing writer reapplies on the winner's tree instead of clobbering it. No defects found; nothing changed.

  • CONFIRMED-OPEN: none.
  • DISMISSED: writeChains in asset-write.ts never deletes completed entries, but the map is keyed by asset id (a bounded, long-lived set), not per-request — not a real leak.
  • DISMISSED: errors from commitLatestAgentAssetSnapshot/commitAgentCapabilityAdd (including the retry-exhaustion Error) aren't caught locally in the two route files, but that matches the pre-existing pattern for writeAndDeployAgentDefinition failures — no new gap.
  • DISMISSED: exactOptionalPropertyTypes handling in capability-add.ts uses the correct conditional-spread pattern for afterWrite and model.
  • DISMISSED: shared-file risk with PR Rename load_skill to skills_load (CL-6455) #603 — Rename load_skill to skills_load (CL-6455) #603's only routes.test.ts hunk touches a single assertion at line ~1380 (unrelated version bump); this PR's new tests are appended at the end of the file. Non-overlapping hunks, trivial rebase either order.

Tests run (from packages/agent-directory): bun test test/routes.test.ts — 62 pass, 0 fail. bun run typecheck — clean.

@linear-code

linear-code Bot commented Sep 4, 2026

Copy link
Copy Markdown

CL-7216

@TheGreatAxios
TheGreatAxios merged commit b2bc3de into main Sep 4, 2026
7 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.

1 participant