Skip to content

fix(errors): refuse issue moves from a state the client no longer sees - #2322

Merged
Makisuo merged 2 commits into
mainfrom
fix/issue-transition-expected-state
Nov 6, 2026
Merged

Makisuo merged 2 commits into
mainfrom
fix/issue-transition-expected-state

Conversation

@Makisuo

@Makisuo Makisuo commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #2310. Local verification there (comment) found that a stale tab could overwrite another tab's move whenever its move was valid from the current state, and that the 409 toast read "An unexpected error occurred".

What changed

  • Transition requests take an optional expected state: expectedState on the internal API, expected_state on v2. If the issue has moved since, the request gets a 409 and writes nothing, including the in_progress lease. Without it, behavior is unchanged. A stale move to the state the issue already has is still a no-op.
  • The in_progress lease is now guarded on the state the move was validated from, so a move that loses its race takes no lease.
  • ErrorIssueTransitionError is now an HttpTaggedError (409, error_issue_transition_conflict, public message). The client's displayError found no public body on the old plain Schema.TaggedError, so it fell back to the unexpected-error text. Wire fields are unchanged.
  • Dashboard: the issue page and the errors list send the state they show. On a 409 they toast the server's message and re-read, so the losing tab shows the state that won. A list row with a write still in flight sends no expectation, so a quick second pick is not refused.
  • Regenerated apps/ios openapi.json for the new v2 field. The anticipated-errors check passes with no change.

The MCP transition_error_issue tool is unchanged; agents read and then move, so it does not take an expected state yet.

Tests

packages/backend  vitest ErrorsService.test.ts -t "expectedState|lease renewal|claim"   20 passed
  refuses a move from a state the issue has left, and writes nothing (no state, lease or event change)
  applies the move when the issue is still in the expected state
  a stale move to the state the issue already has stays a no-op
packages/backend  vitest ErrorIssueWorkflowService.test.ts                             13 passed
apps/web          vitest issue/writes.scene.test.ts hub/selection.scene.test.ts        16 passed
apps/api          vitest v2/phase1-resources.http.test.ts                              39 passed
packages/domain   vitest v2/openapi, v2/v2-contract, error-policy                      69 passed

Typecheck is clean for domain, backend, api, web and ai. oxlint -c .oxlintrc.effect.json is clean on the changed files.

Conflict toast (issue page, rendered through the real kit toast in the web lab; the descriptions are toLoadFailure output for the decoded 409 under each error class)

Before After
before after

I did not run the two-tab check against a live local stack. Port 5499 belongs to another worktree's running stack, so the 409 path is covered by the PGlite test and the client decode tests above.

🤖 Generated with Claude Code


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.

A transition request can now carry the workflow state the client last saw
(expectedState on the internal API, expected_state on v2). When the issue has
since moved, the request is a 409 and nothing is written, so a stale tab no
longer silently overwrites another tab's move.

ErrorIssueTransitionError is now self-describing, so the dashboard toasts the
server's message instead of "An unexpected error occurred", and a 409 re-reads
the issue so the losing tab shows the state that won.
The in_progress lease is now guarded on the state the move was validated
from, a stale move to the state the issue already has stays a no-op, and a
hub row with a write still out sends no expected state.
@maple-review-bot

maple-review-bot Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 10/10 · safe to merge. The guard is checked before the lease and applyTransition re-checks the state atomically; lease and apply are still separate transactions (pre-existing).
no findings · 1/1 new units observable

Adds an optional expectedState to issue transitions on the API, the v2 endpoint and the issue surfaces, so a stale client gets a 409 that writes nothing, plus a lease guard and a 409-typed HttpTaggedError for the UI. Reads correctly; I found no defect."

  • transitionIssue refuses a request whose expectedState no longer matches, before any write
  • acquireLease takes whileState, so a move that lost its race writes no lease
  • ErrorIssueTransitionError becomes an HttpTaggedError (409, public message) the toast can read
  • Issue page and errors list send the state they show and re-read on 409

b21da45 · What was checked, coverage and telemetry details are on the Maple / review check. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

@coderabbitai

coderabbitai Bot commented Nov 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 8 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2de511ee-9b82-4dfc-8088-a3833a324238
Commits

Reviewing files that changed from the base of the PR and between a132495 and b21da45.

Files selected for processing: 16

  • apps/api/src/routes/internal/errors.http.ts
  • apps/api/src/routes/v2/error-issues.http.ts
  • apps/api/src/routes/v2/phase1-resources.http.test.ts
  • apps/ios/Packages/MapleAPI/Sources/MapleAPI/openapi.json
  • apps/web/src/page/errors/hub/command.ts
  • apps/web/src/page/errors/hub/selection.scene.test.ts
  • apps/web/src/page/errors/hub/selection.ts
  • apps/web/src/page/errors/hub/writes.ts
  • apps/web/src/page/errors/issue/command.ts
  • apps/web/src/page/errors/issue/message.ts
  • apps/web/src/page/errors/issue/writes.scene.test.ts
  • apps/web/src/page/errors/issue/writes.ts
  • packages/backend/src/services/errors/ErrorsService.test.ts
  • packages/backend/src/services/errors/ErrorsService.ts
  • packages/domain/src/http/errors.ts
  • packages/domain/src/http/v2/error-issues.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@Makisuo
Makisuo merged commit 4afe283 into main Nov 6, 2026
41 checks passed
@Makisuo
Makisuo deleted the fix/issue-transition-expected-state branch November 6, 2026 20:15
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