Skip to content

fix(labeler): keep assessment-pending live until the last concurrent run resolves (held review finding #13)#2168

Merged
ascorbic merged 2 commits into
feat/plugin-registry-labelling-servicefrom
fix/labeler-sol-13-concurrency
Jul 22, 2026
Merged

fix(labeler): keep assessment-pending live until the last concurrent run resolves (held review finding #13)#2168
ascorbic merged 2 commits into
feat/plugin-registry-labelling-servicefrom
fix/labeler-sol-13-concurrency

Conversation

@ascorbic

@ascorbic ascorbic commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Closes held finding #13 from the adversarial review of the labeler (RFC #694, umbrella #1909) — parked during the earlier fix PRs, now addressed off the merged tip. TDD'd and independently adversary-reviewed (PASS, no #2115 regression). Targets the feat/plugin-registry-labelling-service integration branch.

The race. assessment-pending is a real atproto label whose subject state is the highest-sequence op per (src,uri,val). Concurrent assessment runs for the same (uri,cid) (different run keys → separate Workflow instances, by design so an operator rerun mints a fresh run) each issue their own positive/negation, but all target the same val — so any run's negation becomes the stream head and clears the gate for the whole subject. If run A finalizes passed while run B is still active, A's negation clears the pending gate, briefly flipping the release from pending to eligible (exposing A's stale pass) until B finalizes and re-blocks.

The fix — a pending projection via last-run-clears. The assessment-pending negation is now gated, in the same db.batch, on no other non-terminal run existing for the same (uri,cid) (a new requireNoOtherActiveRun guard on buildIssuanceStatements, alongside the existing signing / requireAssessmentState / subject-not-deleted guards). D1 serializes batch transactions and each run's running→terminal CAS commits in the same batch, so the last run to resolve is the only one that sees no non-terminal sibling and negates — the gate clears exactly once, when the last run finishes. Self-exclusion is by run id, so it holds regardless of batch ordering. Keyed on (uri,cid), so distinct CID versions never entangle. Serialization was rejected in favor of the projection because it would regress the deliberate rerun-mints-a-fresh-run semantics.

Because a run can now finalize passed/blocked while still holding a live positive (its own negation suppressed), the delete cleanup's pending scan (listPendingBearingAssessmentsForUri) is made label-driven — it now negates any run of any state that holds a committed, un-negated positive, plus all non-terminal runs — so the #2115 delete-vs-issuance closure is preserved (the adversary confirmed it's a strict superset of the old state-based scan). The cleanup's cancel-skip widens from state==="stale" to all terminal states (a terminal run is negated but not fed an illegal →cancelled transition).

Known follow-up (deferred, not a regression): the pure CID-supersession case — a run self-staled because its CID was superseded (no delete event) — leaves an old-CID positive that this fix doesn't negate. Negating it would clobber the new CID's legitimate pending (the negation would be the CID-mismatched stream head). Both real consumers (enforcement and getActiveLabelState) are CID-scoped, and the new-CID run's own pending is the stream winner, so the lingering old-CID positive is a stale audit signal on a superseded CID, not a false-eligibility signal. Tracked as a CID-aware pending-reduction follow-up if the audit-view wart matters.

Type of change

  • Bug fix
  • Feature (requires a Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes (all three labeler projects)
  • pnpm lint passes (0 diagnostics)
  • pnpm test passes (labeler: 904 + 136 + 39) — TDD: the pending-gate test asserts A's negation is suppressed while a sibling is active (fails pre-fix, where finalize unconditionally negated); the delete test asserts a terminal decision run's live positive is negated on delete (fails pre-fix, where the state-based scan excluded passed)
  • pnpm format has been run
  • User-visible strings in the admin UI are wrapped for translation (if applicable) — n/a: server-side.
  • I have added a changeset (if this PR changes a published package) — n/a: @emdash-cms/labeler is private.
  • New features link to an approved Discussion — n/a: bug fix; umbrella Plugin registry labelling service #1909 tracks RFC RFC: Decentralized Plugin Registry #694.

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 (implementation + independent adversarial review), orchestrated by Claude Opus 4.8

Screenshots / test output

labeler: 904 (workerd) + 136 (console) + 39 (calibration) passed
adversary: PASS — no stranding/both-skip; label-driven scan is a strict superset (no #2115 regression); read-back can't drop a signing-paused negation; supersession deferral verified CID-safe

Try this PR

Open a fresh playground →

A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.

Tracks fix/labeler-sol-13-concurrency. Updated automatically when the playground redeploys.

…run resolves

Concurrent assessment runs for the same (uri,cid) all target the single
assessment-pending label, so one run's finalization negation cleared the
shared gate while a sibling run was still active — briefly exposing a stale
pass. Gate the pending negation, in-batch, on no other non-terminal run for the
subject (requireNoOtherActiveRun), so only the last run to resolve clears it.
Make the delete cleanup's pending scan label-driven (any run holding a
committed un-negated positive, at any state) so a decision run that now
finalizes while still holding a positive is still negated on delete.
@changeset-bot

changeset-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 8322358

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet size/L cla: signed labels Jul 21, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 21, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-do 8322358 Jul 21 2026, 07:06 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 21, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache 8322358 Jul 21 2026, 07:04 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 21, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground 8322358 Jul 21 2026, 07:06 PM

@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 21, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right fix for finding #13. Gating the assessment-pending negation on “no other non-terminal run for the same (uri, cid)” inside the same db.batch as the running → terminal CAS is a clean projection that preserves the deliberate “rerun mints a fresh run” semantics. Making the delete cleanup’s scan label-driven (all non-terminal runs plus any terminal run still holding a live positive) correctly closes the delete-vs-issuance gap created when a terminal run suppresses its own pending negation because a sibling is still active.

I traced the SQL bind ordering in buildIssuanceStatements, the placement of pendingNegation.statements in the finalization batch versus the assessment-update CAS, the new listPendingBearingAssessmentsForUri predicate, the widened terminal-state cancel-skip in the discovery consumer, and the new TDD tests. I found no logic bug, SQL-injection risk, authorization gap, i18n/RTL issue, missing locale filter, or logged-out query-count impact. One inline comment in the delete handler is now stale after the scan widened beyond terminal stale.


Findings

  • [suggestion] apps/labeler/src/discovery-consumer.ts:625

    This inline comment is stale after the cleanup scan/skip were widened. listPendingBearingAssessmentsForUri now returns all non-terminal runs plus any terminal run holding a live, un-negated positive — not only terminal stale runs — and the skip below is TERMINAL_STATES.has(run.state), not just stale.

    		// Negate (before any cancel) any run — non-terminal or terminal — that
    		// committed a positive pending and has not already been negated.
    

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 21, 2026
@github-actions github-actions Bot added needs-rebase review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jul 21, 2026
…ervice' into fix/labeler-sol-13-concurrency

# Conflicts:
#	apps/labeler/test/assessment-orchestrator.test.ts
@ascorbic

ascorbic commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Fixed the stale comment in 83223581: the negate loop comment now reads "any run — non-terminal or terminal —" to match the widened listPendingBearingAssessmentsForUri scan (any run holding a committed un-negated positive) and the TERMINAL_STATES.has(state) cancel-skip.

That commit is also the reconciliation merge with the integration branch after #2166 (the 9/14/19 batch) merged: the finalization CAS now carries the automation-not-paused guard (14), the subject-not-deleted guard (#2115), and the signing guard together with this PR's requireNoOtherActiveRun on the pending negation; the current-pointer id tie-break (19) is present; and both PRs' tests coexist. Full labeler suite 909 + 136 + 39 green.

~ 🤖 Claude Fable 5

@ascorbic
ascorbic merged commit abe4d72 into feat/plugin-registry-labelling-service Jul 22, 2026
8 checks passed
@ascorbic
ascorbic deleted the fix/labeler-sol-13-concurrency branch July 22, 2026 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla: signed review/awaiting-author Reviewed; waiting on the author to respond size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant