Skip to content

fix(drafts): keep annotation reply visible through concurrent WS update - #226

Merged
prellr merged 1 commit into
mainfrom
fix/drafts-annotation-reply-flicker
Jun 30, 2026
Merged

prellr merged 1 commit into
mainfrom
fix/drafts-annotation-reply-flicker

Conversation

@prellr

@prellr prellr commented Jun 30, 2026

Copy link
Copy Markdown
Owner

Problem

Posting a reply in an annotation thread made the message vanish on Send, then reappear — a flicker, not a data loss.

Root cause

useAddDraftAnnotationMessage optimistically appends a message with a temp id (temp-message-…), then onSettled invalidates the list. But when the reply @-mentions Aye it triggers a turn, so a concurrent draft_annotation:updated WS event arrives mid-flight. onDraftAnnotationUpdated replaced the whole annotation with the WS payload (whose messages are the server's, without the in-flight optimistic reply) → the optimistic message was dropped. The later refetch/echo brought the committed copy back, producing the flicker.

A latent second bug: onDraftAnnotationMessageCreated deduped only by id, but the optimistic temp id ≠ the server id, so the user's own message-created echo would have appended a duplicate.

Fix (frontend-only, packages/core/drafts/)

  1. Swap optimistic temp → real on success (useAddDraftAnnotationMessage): new onSuccess replaces the optimistic temp message (matched by the temp id carried in the onMutate context) with the real api.addDraftAnnotationMessage response, so the real id is in the cache promptly. onError rollback unchanged; onSettled invalidate kept as a now-non-flickering safety net.
  2. Preserve in-flight optimistic messages across WS annotation rewrites (annotation-ws-updaters.ts): a shared withPreservedTempMessages helper keeps any temp-id messages the incoming WS payload omits, applied in onDraftAnnotationUpdated and the re-match branch of onDraftAnnotationCreated. Adds/exports isTempMessageId (mirrors isTempAnnotationId).
  3. Robust message dedupe (onDraftAnnotationMessageCreated): keeps the id dedupe and, for the window before the temp→real swap, replaces an un-swapped temp message with the same author_user_id+body instead of appending a duplicate.

Tests

packages/core/drafts/annotation-ws-updaters.test.ts and annotation-mutations.test.tsx:

  • Reproduces the original flicker: post a reply (optimistic temp message in cache) → fire onDraftAnnotationUpdated with a payload that lacks it → assert the optimistic reply survives (this is where it used to disappear).
  • Proves no duplicate: then fire onDraftAnnotationMessageCreated with the real row → assert exactly one copy.
  • onSuccess swaps temp→real (real id lands in cache); onError rolls back.

Verification

  • pnpm --filter @multica/core typecheck ✓ · pnpm --filter @multica/views typecheck ✓ · full pnpm typecheck ✓ (6/6)
  • pnpm --filter @multica/core exec vitest run ✓ (42 files, 378 tests) · full pnpm test ✓ (857 views tests, all packages)
  • pnpm --filter @multica/core lint ✓ (no new findings)

No backend/schema/daemon change.

🤖 Generated with Claude Code

Posting a reply in an annotation thread made the message vanish on Send,
then reappear. When the reply @-mentions Aye it triggers a turn, so a
concurrent draft_annotation:updated event lands mid-flight; the WS
updater replaced the whole annotation with the server payload (whose
messages lack the in-flight optimistic reply), dropping it until the
next refetch echoed it back.

- useAddDraftAnnotationMessage: add onSuccess that swaps the optimistic
  temp message for the real server row, so the real id is in the cache
  promptly. onSettled invalidate stays as a (now-non-flickering) safety
  net.
- annotation-ws-updaters: preserve in-flight temp-id messages when a WS
  event replaces an annotation wholesale (onDraftAnnotationUpdated and
  the re-match branch of onDraftAnnotationCreated). Add isTempMessageId.
- onDraftAnnotationMessageCreated: keep id dedupe, and also replace an
  un-swapped temp message with the same author+body instead of appending
  a duplicate echo.
- Tests: optimistic reply survives a mid-flight rewrite, the echo
  reconciles to exactly one row, and onSuccess swaps temp->real.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@prellr
prellr merged commit 2b3a873 into main Jun 30, 2026
2 checks passed
@prellr
prellr deleted the fix/drafts-annotation-reply-flicker branch June 30, 2026 04:12
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