Where. frontend/taskdeck-web/src/composables/useProposalRevisions.ts, the saveRevision early return for a non-current save (main line ~140; PR #2441 line ~153). Found by the independent verification of a Codex P1 thread on PR #2441; the gap pre-exists that PR (main has the identical early return), so it is tracked here rather than fixed in the PR.
Race. A save POST for proposal A is in flight. The reviewer navigates A → B → A. Each switch bumps saveGeneration and loadGeneration, and the return to A issues a fresh revisions GET whose server-side read can execute before the POST commits, so it returns an empty list. The POST then resolves; current is false, and the persisted-but-not-current branch returns without bumping loadGeneration. The stale GET resolves last: its generation still matches, so it writes revisionCount = 0, latestRevision = null, revisionsLoaded = true, certifying "A has no revisions" although one is persisted.
Effect. For a zero-original-operation proposal the view shows the false "invalid, no operations" surface and the Apply guard short-circuits with the zero-op toast; editablePayload falls back to the original operations. No data is lost server-side. The indeterminate path in #2441 invalidates on the same condition; the persisted path does not, which is the asymmetry.
Smallest fix (not applied). In the non-current persisted branch, when activeProposal.value?.id === proposalId, bump loadGeneration, set revisionsLoaded = false, and re-issue loadRevisionState(proposalId) after the POST resolved so it observes the commit. Do not increment revisionCount (the watcher already zeroed it for the reopened session). Add a spec that interleaves a slow createRevision with A→B→A and a stale getRevisions resolution.
Severity. Compound window (slow save surviving two selections, GET snapshot before commit, GET resolving after the POST continuation). Non-blocking under the causal bar; part of #2215's residual list.
Where.
frontend/taskdeck-web/src/composables/useProposalRevisions.ts, thesaveRevisionearly return for a non-current save (mainline ~140; PR #2441 line ~153). Found by the independent verification of a Codex P1 thread on PR #2441; the gap pre-exists that PR (mainhas the identical early return), so it is tracked here rather than fixed in the PR.Race. A save POST for proposal A is in flight. The reviewer navigates A → B → A. Each switch bumps
saveGenerationandloadGeneration, and the return to A issues a fresh revisions GET whose server-side read can execute before the POST commits, so it returns an empty list. The POST then resolves;currentis false, and the persisted-but-not-current branch returns without bumpingloadGeneration. The stale GET resolves last: its generation still matches, so it writesrevisionCount = 0,latestRevision = null,revisionsLoaded = true, certifying "A has no revisions" although one is persisted.Effect. For a zero-original-operation proposal the view shows the false "invalid, no operations" surface and the Apply guard short-circuits with the zero-op toast;
editablePayloadfalls back to the original operations. No data is lost server-side. The indeterminate path in #2441 invalidates on the same condition; the persisted path does not, which is the asymmetry.Smallest fix (not applied). In the non-current persisted branch, when
activeProposal.value?.id === proposalId, bumploadGeneration, setrevisionsLoaded = false, and re-issueloadRevisionState(proposalId)after the POST resolved so it observes the commit. Do not incrementrevisionCount(the watcher already zeroed it for the reopened session). Add a spec that interleaves a slowcreateRevisionwith A→B→A and a stalegetRevisionsresolution.Severity. Compound window (slow save surviving two selections, GET snapshot before commit, GET resolving after the POST continuation). Non-blocking under the causal bar; part of #2215's residual list.