fix(web): stop a task status from reverting when an older update finishes late - #3528
fix(web): stop a task status from reverting when an older update finishes late#3528nova28 wants to merge 4 commits into
Conversation
A stale failed status/priority/assignee mutation restored its pre-mutation snapshot unconditionally, clobbering a newer, already-succeeded mutation for the same task (e.g. two fast consecutive board drags). Extend the existing per-task write-guard sequence used by the title/description writers to a task-scoped write, so a rollback is skipped once a later mutation has already succeeded or is still in flight.
…upersession The picker caught ApprovalGateError and re-applied the redirected status via a second, unguarded mutate() call, so a stale gate redirect could clobber a newer, already-succeeded status change. Move the redirect into useOptimisticTaskMutation itself, gated by the same shouldRestoreAfterFailedWrite check the rollback already uses, and delete the picker's special-cased catch. Also tighten the property-pickers e2e regression to read the backend's settled status before asserting the UI label, so the assertion can't pass on a still-optimistic value ahead of a buggy rollback.
…ecks An older, still-in-flight mutation failing after a newer one hit the approval gate could clobber the gate's server-confirmed redirected status: both writers settled the UI onto the redirect but never recorded it as a settled write, so the older failure's rollback check saw nothing newer and restored its stale snapshot over it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Warning Review limit reachedNext included review available in 15 minutes. View limit detailsLimit details: You’ve used all 4 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: QUIET Plan: Advanced Run ID: 📒 Files selected for processing (9)
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. Comment |
|
Claude finished @nova28's task in 5m 10s —— View job Code Review
|
| Severity | Count |
|---|---|
| Blocker | 0 |
| Suggestion | 2 (same root cause) |
Verdict: Ready with suggestions — the generation guard and its tests are correct; the only open issue is the acknowledged local-state/store divergence on the rollback path, which the PR itself flags as a follow-up.
|
| Filename | Overview |
|---|---|
| apps/web/lib/state/office-task-content-sync.ts | Widens write guards to a whole-task scope, but permanently discards suppressed failure recovery needed when all overlapping writes fail. |
| apps/web/hooks/use-optimistic-task-mutation.ts | Applies task-scoped sequencing and approval redirects to generic mutations, while retaining issue-time snapshot rollback semantics that expose the double-failure ordering bug. |
| apps/web/app/office/tasks/use-board-drag.ts | Adds the same task-scoped guard to status drops and therefore shares the unresolved rollback-ordering failure. |
| apps/web/components/task/simple/components/status-picker.tsx | Removes component-specific approval redirect recovery in favor of the shared mutation hook. |
| apps/web/hooks/use-optimistic-task-mutation-rollback-ordering.test.tsx | Adds substantial overlap coverage, but omits the older-fails-first/newer-fails-later ordering. |
| apps/web/app/office/tasks/use-board-drag.test.ts | Covers late stale failures and approval redirects but not sequential failure in the opposite completion order. |
| apps/web/e2e/tests/office/property-pickers.spec.ts | Adds realistic browser coverage for an older failure arriving after a newer successful status update. |
Sequence Diagram
sequenceDiagram
participant UI
participant Guard
participant A as Older request A
participant B as Newer request B
UI->>Guard: Begin A and apply optimistic value A
UI->>Guard: Begin B and snapshot value A
A-->>UI: Failure
UI->>Guard: Restore A?
Guard-->>UI: No, B is pending
UI->>Guard: End A and discard its rollback
B-->>UI: Failure
UI->>Guard: Restore B?
Guard-->>UI: Yes
UI->>UI: Restore B snapshot containing value A
Note over UI: Both requests failed, but value A remains visible
Reviews (1): Last reviewed commit: "fix: record settlement for approval-gate..." | Re-trigger Greptile
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ab90472ac4
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Checked — this doesn't reproduce. const restoreTask = useCallback(
(snapshot: Task) => {
setTask((prev) =>
prev && prev.id === id
? { ...snapshot, title: prev.title, description: prev.description }
: prev,
);
},
[id, setTask],
);It spreads |

Tip
PR walkthrough: Open the visual walkthrough
Today: Dragging a task on the Office board (or editing its status/priority/assignee through a
property picker) twice in quick succession can leave the UI showing a stale value: if an older
request fails after a newer one for the same task already succeeded, the older failure's rollback
overwrites the newer, server-confirmed state — and it stays wrong until an unrelated refetch or a
page reload.
After this: A failed mutation only rolls back its optimistic patch when it is still the last
word for that task. Once a newer mutation has settled successfully, a late failure surfaces its
error toast but no longer clobbers the board.
Who hits this: Anyone re-triaging tasks quickly on the Office kanban board, or clicking through
property pickers (status, priority, assignee, project, parent, blockers, reviewers, approvers)
faster than the network round-trip.
Scope: standalone fix, zero backend, 9 files under
apps/web/.Not here: Interference between two different properties changed by two different in-flight
mutations on the same task (e.g. priority fails after status succeeds) is not separately guarded —
the whole-task write-guard scope this PR adds relies on the existing
task.updatedWebSocketbroadcast + refetch to reconcile that case, which was verified safe but is undocumented and
untested. That's tracked as a follow-up rather than folded into this PR.
Important Changes
lib/state/office-task-content-sync.tswith awhole-task
TASK_SCOPE, souseOptimisticTaskMutationand the board'sapplyStatusDropbothcompare a monotonic per-task sequence before restoring a snapshot on failure, instead of
restoring unconditionally.
recordWriteSettled: the approval-gate redirect path now records its server-persistedstatus as settled before deciding whether to roll back, so a gate redirect isn't lost even when
a newer mutation is still in flight.
Validation
make typecheck— cleanmake lint-backend(0 issues) /make lint-web(eslint --max-warnings 0, clean) /make lint-format— cleanpnpm run i18n:ratchet(fromapps/web) — clean, 0 added + 4 modified filesvitest— 6 files, 99 tests passed (use-board-drag.test.ts,status-picker.test.tsx,use-optimistic-task-mutation-rollback-ordering.test.tsx,office-task-content-sync.test.ts,use-optimistic-task-mutation.test.tsx,office-tasks.test.ts)tests/office/property-pickers.spec.ts— 13 passed;tests/office/tasks.spec.ts— 12 passedmain; all of the above stayed greenThree unrelated failures on this runner, none touched by this diff:
internal/worktree(Go): identical failure set reproduced against the merge-base commit in ascratch worktree — a temp-directory/filesystem quirk on this machine, not a regression.
apps/web/lib/http-git-server.test.ts: 3 failures require a running Docker daemon; this runnerintentionally does not run Docker.
make lint-harness/lint-specs: this runner's Python is 3.9.6, which doesn't support thestr | Nonesyntax those scripts use (needs 3.10+); the scripts themselves are unchanged by thisdiff.
Possible Improvements
Low risk: the whole-task write-guard is coarser than per-field, but the coarser scope was verified
safe against the WS reconciliation path rather than assumed; a follow-up card tightens the scope and
adds the missing test/doc coverage for that invariant.
Checklist
apps/web/), I have added or updated Playwright e2e tests inapps/web/e2e/and verified them withmake test-e2e.docs/public/**and updated them or noted why no docs change is needed.Preview Environment
ab90472