Skip to content

Make comments and suggested edits resolve optimistically - #5722

Open
3mdistal wants to merge 8 commits into
mainfrom
t3code/fix-comment-paste-optimism
Open

3mdistal wants to merge 8 commits into
mainfrom
t3code/fix-comment-paste-optimism

Conversation

@3mdistal

@3mdistal 3mdistal commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Problem

When a Content collaborator submits a comment or reply, the new entry appears optimistically while the same text remains in a disabled composer until the server responds. That makes one submission look duplicated and slows down the next reply. Core review comments and suggested-edit decisions also wait for server results in several paths.

Approach

Transfer each submitted draft to one optimistic entry and clear its composer immediately. Keep the submitted snapshot separate from new typing so a late response cannot erase a newer draft. Use a stable operation ID to reconcile server echoes and make Core comment and reply retries safe.

What changed

  • Content root comments, ordinary replies, and suggestion replies share an operation-aware draft lifecycle and hand off from the pending anchor to the optimistic thread without a second copy of the text.
  • Core review hooks project create, reply, edit, resolve, reopen, delete, and suggestion decisions locally, then reconcile or roll back the affected operation. The shared review panel clears submitted composers and restores them on definite failure.
  • Core create and reply actions accept an optional client operation UUID. An exact retry returns the existing comment without sending another notification; a conflicting payload is rejected.
  • Suggested-edit decisions gate only the affected suggestion. Per-invocation cleanup keeps concurrent decisions and thread actions from leaving controls stuck.
  • Pending overlays rebase on fresh query results, preserve newer server data, and refresh review feedback after settlement. List-limited comment views keep the server's thread-activity ordering.
  • Ambiguous Content submissions can be checked and retried with the same operation ID. Same-turn submission guards prevent duplicate creates and thread resolution requests; reply replay also validates its durable routing target.

Verification

  • Focused Core and Content suites passed for delayed submissions, draft restoration, refetch rebasing, same-turn guards, stale responses, and idempotent retries. Core and Content typechecks, the Core build, git diff --check, and the changed-code error-handling guard passed.
  • Independent bounded review found a concurrent mutation callback race; the final code fixes it and its focused regression passes. GitHub CI is running on the current PR head.
  • Real-interface Content acceptance remains unverified. The local dev server returned Nitro's Vite environment "nitro" is unavailable page, and the T3 preview could not reach the editor. The Windows Content build compiled the client and server bundles but failed in its final packaging step while invoking the extensionless local esbuild binary. Neither result is counted as a UI pass.

Review focus

  • Check that optimistic overlays reconcile stale reads and out-of-order results without duplicating or erasing comments. Suggestion IDs are server-generated, so an intervening refetch can show a transient duplicate until the create response reconciles it.
  • Check retry identity, permission scope, and notification behavior in Core review actions.
  • Check suggestion decision rollback against canonical document state and concurrent typing.

The reported Command-V failure also occurs on another website in T3 Code. The Content comment textarea does not intercept paste, and Alice excluded that separate host issue from this PR.

content_product_impact:
  lane: contract_repair
  features:
    - content.feature.collaborate-in-context
    - content.feature.review-changes-in-place
  capabilities:
    - content.comment.page-owned
    - content.revision.suggestions
  record_change: none
  proof:
    - focused Content comment draft and sidebar interaction tests
    - focused Core review client, panel, action, and store tests
  rationale: This repairs the immediate review interaction and retry behavior without changing the product contract.

@github-actions

github-actions Bot commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Here's a visual recap of what changed:

Visual recap

Open the full interactive recap

builder-io-integration[bot]

This comment was marked as outdated.

builder-io-integration[bot]

This comment was marked as outdated.

builder-io-integration[bot]

This comment was marked as outdated.

builder-io-integration[bot]

This comment was marked as outdated.

builder-io-integration[bot]

This comment was marked as outdated.

@builder-io-integration builder-io-integration 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.

Builder reviewed your changes and found 8 potential issues 🟡

Review Details

Code Review Summary

This incremental review covers the latest retry bookkeeping, receipt validation, optimistic suggestion reconciliation, and composer concurrency changes. The three previously open findings were verified fixed: reply routing is now included in the idempotency receipt, same-thread resolution uses a synchronous in-flight guard, and refetched review records are deduplicated before overlays are applied. Those comments were resolved before this review.

New Findings

  • 🟡 MEDIUM — Retry records are shared per composer, so a late success from submission A can erase submission B's operation ID and make an ambiguous retry non-idempotent.
  • 🟡 MEDIUM — Failed submissions are dropped when newer text exists in the composer, losing the original text after optimistic rollback.
  • 🟡 MEDIUM — Same-composer submissions are not kept in flight-locked, allowing a user to type and submit a second comment/reply before the first settles.
  • 🟡 MEDIUM — Receipt matching includes mutable access/display snapshots, causing valid retries to be rejected after profile or ACL-context changes.
  • 🟡 MEDIUM — A stale suggestion amendment response can overwrite a newer refetched revision.
  • 🟡 MEDIUM — A replay after notification failure suppresses notification permanently.

The remaining approach is directionally sound and the new fixes address several prior race conditions, but retry state needs operation-level ownership rather than composer-level ownership. Risk level: high because this affects shared mutation, notification, and idempotency semantics.

🧪 Browser testing: Will run after this review (PR touches UI code)

);
clientOperationId: operationId,
});
createRetryRef.current = null;

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.

🟡 Do not clear another submission's retry identity

createRetryRef is shared by all root submissions, but any successful request clears it here. If submission B becomes ambiguous first and stores its operation ID, then an older concurrent submission A succeeds, retrying B generates a new UUID and can duplicate a comment that was already committed. Associate retry state with the specific operation/payload and clear it only when that same operation settles; the reply retry map has the same race.

Additional Info
Reported independently by 1 of 4 agents and confirmed against the shared retry ref.

Fix in Builder

Comment on lines +435 to +439
if (
draftGenerationRef.current !== generation ||
draftRef.current !== ""
) {
return;

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.

🟡 Retain failed submissions when newer typing exists

When a request fails after the user has typed a newer draft, this branch returns without retaining the submitted snapshot. The optimistic entry is rolled back, so the original comment text is silently lost and cannot be retried. Store failures by operation while leaving the newer draft editable; the reply path has the same loss condition.

Additional Info
Reported independently by 1 of 4 agents; confirmed from rollback behavior and this early return.

Fix in Builder

setSubmittingTarget(resolutionTarget);
createComment.mutate(
{
const submitDraft = async (resolutionTarget: ReviewResolutionTarget) => {

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.

🟡 Keep same-composer submissions in flight-locked

Clearing the draft prevents an immediate duplicate click, but it does not prevent the user from typing again and submitting from the same root composer while the first request is pending. The second submission receives a new UUID and persists a second comment; the same applies to a reply composer. Track an in-flight root submission and per-thread reply submissions without blocking unrelated composers.

Additional Info
Reported by 1 of 4 agents; this is distinct from the resolved same-thread resolution guard.

Fix in Builder

Comment on lines +724 to +742
const immutableFields: (keyof ReviewComment)[] = [
"id",
"resourceType",
"resourceId",
"threadId",
"parentCommentId",
"targetId",
"kind",
"anchor",
"body",
"authorEmail",
"authorName",
"createdBy",
"resolutionTarget",
"mentions",
"ownerEmail",
"orgId",
"visibility",
"metadata",

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.

🟡 Exclude mutable context from retry receipt matching

The retry fingerprint includes authorName, ownerEmail, orgId, and visibility, although these are recomputed from current profile/access context on every retry. A valid retry after a display-name, ownership, organization, or visibility-context change is rejected as a conflicting operation even though the immutable submission is identical. Match immutable submission fields and re-authorize the retry separately.

Additional Info
Reported by 1 of 4 agents; confirmed against the action context fields used to construct the receipt.

Fix in Builder

Comment on lines +1061 to +1065
onSuccess: (result) => (data) =>
updateSuggestions(data, (suggestions) =>
suggestions.map((suggestion) =>
suggestion.id === input.id
? (result as ResourceSuggestion)

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.

🟡 Do not overwrite newer suggestion revisions with stale responses

The successful amendment transform unconditionally replaces the cached suggestion with the mutation result. If a refetch delivers a newer revision while the original request is in flight, a late older response temporarily restores stale operations/revision data until invalidation completes. Preserve the newer record using revision or updated-at ordering.

Additional Info
Reported by 1 of 4 agents; confirmed against the adjacent revision-aware reconciliation logic.

Fix in Builder

Comment on lines +99 to +103
return {
...result.comment,
replayed: result.replayed,
notified: result.replayed
? null

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.

🟡 Retry notification delivery for idempotent replays

Once an idempotent comment row exists, replay returns notified: null and skips notifyReviewComment. If the original request committed the row but notification failed or the response was lost after notification failure, a same-operation retry reports success without ever retrying delivery, permanently losing the notification. Persist notification delivery state or retry it through an idempotent notification path.

Additional Info
Reported by 1 of 4 agents; confirmed against the action's post-insert notification await and replay branch.

Fix in Builder

...(submittedMentions.length ? { mentions: submittedMentions } : {}),
clientOperationId: operationId,
});
delete replyRetriesRef.current[comment.id];

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.

🟡 Do not clear another reply's retry identity

The retry slot is keyed only by comment and is cleared by any successful reply for that comment. If reply B becomes ambiguous first and reply A succeeds later, this deletes B's operation ID; retrying B then uses a new UUID and can duplicate the persisted reply. Track retry state by operation/payload and clear only the matching submission.

Additional Info
Reported by 1 of 4 agents; confirmed against concurrent same-thread reply behavior.

Fix in Builder

Comment on lines +550 to +555
} catch {
if (
(replyGenerationsRef.current[comment.id] ?? 0) !== generation ||
(replyDraftsRef.current[comment.id] ?? "") !== ""
) {
return;

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.

🟡 Retain failed replies when newer typing exists

If a reply fails after newer text has been entered in that thread, this branch returns without retaining the submitted reply snapshot. Optimistic rollback then leaves neither the failed reply nor recoverable retry text, silently losing the original submission. Keep operation-scoped failure state separate from the current draft.

Additional Info
Reported by 1 of 4 agents; confirmed from the reply rollback path.

Fix in Builder

This branch was successfully deployed

13 active deployments
pr-5722-design fad4c0ce Deployed Sep 23, 2026 by github-actions[bot]
pr-5722-mail fad4c0ce Deployed Sep 23, 2026 by github-actions[bot]
pr-5722-fw fad4c0ce Deployed Sep 23, 2026 by github-actions[bot]
pr-5722-forms fad4c0ce Deployed Sep 23, 2026 by github-actions[bot]
pr-5722-calendar fad4c0ce Deployed Sep 23, 2026 by github-actions[bot]
pr-5722-content fad4c0ce Deployed Sep 23, 2026 by github-actions[bot]
pr-5722-starter fad4c0ce Deployed Sep 23, 2026 by github-actions[bot]
pr-5722-analytics fad4c0ce Deployed Sep 23, 2026 by github-actions[bot]
pr-5722-plan fad4c0ce Deployed Sep 23, 2026 by github-actions[bot]
pr-5722-assets fad4c0ce Deployed Sep 23, 2026 by github-actions[bot]
pr-5722-slides fad4c0ce Deployed Sep 23, 2026 by github-actions[bot]
pr-5722-clips fad4c0ce Deployed Sep 23, 2026 by github-actions[bot]
pr-5722-dispatch fad4c0ce Deployed Sep 23, 2026 by github-actions[bot]
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