Skip to content

fix(lint): render-sync freshness-critical latest-value refs (#1063 follow-up) - #1069

Merged
xiaolai merged 2 commits into
mainfrom
fix/react-hooks-7-audit-followup
Jun 27, 2026
Merged

xiaolai merged 2 commits into
mainfrom
fix/react-hooks-7-audit-followup

Conversation

@xiaolai

@xiaolai xiaolai commented Jun 27, 2026

Copy link
Copy Markdown
Owner

Follow-up to #1068. A two-round cross-model audit (Codex) of the merged react-hooks-7 adoption found that several "latest-value" refs were moved from a render-phase write into a post-commit useEffect — which introduces a commit-to-passive-flush staleness window. For refs read by synchronous handlers or timers, that window is a real bug.

Findings fixed (4, across 2 rounds)

Round File Severity Issue
1 TiptapEditor.tsx High 6 refs read by the deferred init-parse setTimeout + the #755 unmount-flush — could read stale content and initialize the editor with old text
1 useGenieInvocation.ts Low scoped-disable reason was inaccurate (MCP bridge handler is synchronous, not async) — comment corrected
2 SourceEditor.tsx High hiddenRef/cursorInfoRef read by CodeMirror's update listener, a delayed focus/restore setTimeout, and an interval poll — stale hiddenRef could let a hidden editor write/focus; stale cursorInfoRef restores the wrong cursor
2 useTabDragOut.ts Medium drag refs read by synchronous document pointer listeners during a drag

All four reverted to render-phase sync with a block-scoped react-hooks/refs disable — the original, correct behavior.

Deliberately left as passive effects

SourcePane (onDiagnosticsRef), useContentServer (startServerRef), useTerminalSessions (callbacksRef) were audited and confirmed safe: their readers are async and the mirrored callbacks are stable, so the commit-to-passive window is harmless. This is the correct per-case split — freshness-critical refs are render-synced; stable async-only refs stay in effects (more concurrent-safe).

Verification

Each fix independently re-verified FIXED by a fresh Codex pass. Full eslint src clean, tsc clean, file-size at baseline, 275 affected tests pass.

Refs #1063

xiaolai added 2 commits June 27, 2026 19:43
…#1063)

Second cross-model audit round caught a real regression the first pass missed:
the six latest-value refs were moved to a passive useEffect, but the deferred
init parse (setTimeout) reads contentRef/preserveLineBreaksRef to recover from
content drift, and the unmount-flush path (#755) also needs pre-effect freshness.
A passive effect has nondeterministic ordering versus setTimeout(0), risking a
stale read that initializes the editor with old content. Revert to a render-phase
sync (block-scoped react-hooks/refs disable) — the original, correct behavior.

Also corrects the useGenieInvocation disable reason: the MCP bridge handler is a
synchronous CustomEvent handler (not async), so the render-phase write is needed
for the synchronous handshake to see the latest invokeGenie.
…#1063)

Round 2 of the cross-model audit found two more refs with the same hazard as
TiptapEditor — moved to a passive effect but read from paths that can run in
the commit-to-passive-flush gap:

- SourceEditor: hiddenRef/cursorInfoRef (+ setter refs) are read by CodeMirror's
  update listener, a delayed focus/restore setTimeout, and an interval poll. A
  stale hiddenRef could let a now-hidden editor write/sync/focus; a stale
  cursorInfoRef could restore the previous render's cursor.
- useTabDragOut: the drag callback/bar refs are read by synchronous document
  pointer listeners during an active drag.

Both reverted to render-phase sync (block-scoped react-hooks/refs disable). The
other passive-synced refs (SourcePane, useContentServer, useTerminalSessions)
were audited and confirmed safe — their readers are async and the mirrored
callbacks are stable, so the commit-to-passive window is harmless there.
@xiaolai
xiaolai merged commit 1ffdc20 into main Jun 27, 2026
7 checks passed
@xiaolai
xiaolai deleted the fix/react-hooks-7-audit-followup branch June 27, 2026 12:24
xiaolai added a commit that referenced this pull request Aug 12, 2026
…llow-up) (#1069)

* fix(lint): revert TiptapEditor latest-value refs to render-phase sync (#1063)

Second cross-model audit round caught a real regression the first pass missed:
the six latest-value refs were moved to a passive useEffect, but the deferred
init parse (setTimeout) reads contentRef/preserveLineBreaksRef to recover from
content drift, and the unmount-flush path (#755) also needs pre-effect freshness.
A passive effect has nondeterministic ordering versus setTimeout(0), risking a
stale read that initializes the editor with old content. Revert to a render-phase
sync (block-scoped react-hooks/refs disable) — the original, correct behavior.

Also corrects the useGenieInvocation disable reason: the MCP bridge handler is a
synchronous CustomEvent handler (not async), so the render-phase write is needed
for the synchronous handshake to see the latest invokeGenie.

* fix(lint): render-sync SourceEditor + useTabDragOut latest-value refs (#1063)

Round 2 of the cross-model audit found two more refs with the same hazard as
TiptapEditor — moved to a passive effect but read from paths that can run in
the commit-to-passive-flush gap:

- SourceEditor: hiddenRef/cursorInfoRef (+ setter refs) are read by CodeMirror's
  update listener, a delayed focus/restore setTimeout, and an interval poll. A
  stale hiddenRef could let a now-hidden editor write/sync/focus; a stale
  cursorInfoRef could restore the previous render's cursor.
- useTabDragOut: the drag callback/bar refs are read by synchronous document
  pointer listeners during an active drag.

Both reverted to render-phase sync (block-scoped react-hooks/refs disable). The
other passive-synced refs (SourcePane, useContentServer, useTerminalSessions)
were audited and confirmed safe — their readers are async and the mirrored
callbacks are stable, so the commit-to-passive window is harmless there.
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