Skip to content

fix(core): serialize prompt settlement around event publication - #44340

Closed
sahiljagtap08 wants to merge 1 commit into
anomalyco:devfrom
sahiljagtap08:prompt-settlement-race
Closed

sahiljagtap08 wants to merge 1 commit into
anomalyco:devfrom
sahiljagtap08:prompt-settlement-race

Conversation

@sahiljagtap08

Copy link
Copy Markdown

Issue for this PR

Closes #34853

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

QuestionV2.reply, QuestionV2.reject, and PermissionV2.reply published their settlement event before removing the pending entry. Since EventV2.publish runs listeners inline, a slow listener suspends the first reply while the entry still looks pending. A second concurrent reply then also passes the pending check, publishes a conflicting settlement event, and both callers get success.

The fix claims the pending entry synchronously before publishing, so the second caller gets NotFoundError instead of double settling. If the publish fails, the entry is restored, which keeps the rollback behavior these services already used on the create side. The cascade loops in PermissionV2.reply (reject all in session, auto allow after "always") now re-claim each entry with pending.delete(id) before publishing, since every publish is a suspension point where another reply can win the race.

This works because the claim happens with no yield point between the pending check and the delete, so only one settlement can ever proceed per request.

How did you verify your code works?

Added regression tests that block a settlement listener on a deferred and race a second reply. Both tests fail on the current code (double publish or timeout) and pass with the fix. bun test test/question.test.ts test/permission.test.ts in packages/core: 16 pass. Typecheck and oxlint are clean.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@Enough1122

Copy link
Copy Markdown

AI code review — automated review for reference, author can ignore or act on any point.

Correct fix for a real race — the old order (publish first, settle/delete after) let a second reply slip through pending.get() while listeners delayed the first publish inside events.publish, producing double settlement and duplicate broadcasts. Claiming the entry first, restoring it only when the publish itself fails, is exactly right, and moving pending.delete() before the deferred resolution keeps list-pollers consistent with the broadcast. The Array.from(pending) snapshots in both cascade loops (packages/core/src/permission.ts:~245, ~267) are necessary now that the loop mutates mid-iteration across suspension points. The gated-listener tests are the highlight: they deterministically reproduce the interleaving and assert exactly-one event, NotFoundError for the loser, and an empty final list (packages/core/test/permission.test.ts:286-319, mirrored for questions including reject in question.test.ts:92-128).

Two small suggestions:

  1. The claim→publish→restore-on-error sequence is now copy-pasted four times (permission.ts primary/reject-cascade/always-cascade, question.ts reply/reject). Extracting a settleClaimed(id, item, event) helper would keep the invariant in one place — this is precisely the kind of subtle ordering that drifts in future edits.

  2. Cascade failure path is untested. Both tests gate only the primary publish. A companion test making the second (cascaded) publish fail would pin the recovery semantics: earlier items stay settled, the failing item is restored to pending, and the error propagates without corrupting the map.

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR Cleanup

Thank you for contributing to opencode.

Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions.

This PR was closed because it matched the following cleanup criteria:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

PRs created within the last month are not affected by this cleanup.

If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate.

Thanks again for taking the time to contribute.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(core): serialize V2 prompt settlement around event publication

2 participants