fix: Prevent stale draft restore after message send#707
Conversation
When a message is sent, clearElementValue clears the textarea and __liveDrafts. But on a Blazor re-render, restoreDraftsAndFocus could overwrite the empty textarea with a stale draft from draftBySession if the timing was wrong. Fix: Mark inputs as 'recently sent' via __recentlySentIds when clearElementValue runs. restoreDraftsAndFocus checks this flag and skips restoring non-empty drafts into recently-cleared inputs. The flag is one-shot (cleared after first skip) to allow future legitimate draft restores. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Design-level findings (outside diff)🟢 MINOR —
Discarded findings (single reviewer only)
|
There was a problem hiding this comment.
Code Review Summary — PR #707
Methodology: 3 independent reviewers with adversarial consensus. Disputed findings were verified by follow-up cross-examination.
Findings
| # | Severity | Finding | Consensus |
|---|---|---|---|
| 1 | 🟡 MODERATE | Flag in __recentlySentIds consumed unconditionally on first encounter — protection lost if continue doesn't fire, leaving a window for stale restore on subsequent re-render |
2/3 reviewers |
| 2 | 🟢 MINOR | __recentlySentIds entries leak for multi-agent input IDs that never appear in the drafts iteration loop |
3/3 reviewers |
| 3 | 🟢 MINOR | clearElementValue sets the "recently sent" flag for non-send callers (e.g., HandleDiffCommentAsync) — no functional impact but semantically misleading |
2/3 reviewers |
Discarded: 1 finding (residual pre-enqueue race) — flagged by 1/3, both follow-ups confirmed the race is self-correcting since clearElementValue always follows in the JS FIFO queue.
Overall Assessment
The core fix is well-targeted and correct — it addresses the fire-and-forget race condition where a stale restoreDraftsAndFocus overwrites a cleared textarea after send. The one-shot flag pattern works for the primary scenario. Finding #1 (MODERATE) identifies a narrow window where the flag can be consumed without providing protection, which could be tightened by moving the delete inside the continue branch. Finding #2 is a minor hygiene issue with a straightforward cleanup sweep. Finding #3 is cosmetic.
CI Status: pre_activation ✅ activation ✅ agent 🔄 (this review)
Prior Reviews: None
Test Coverage: PR states all 3,510 tests pass
Generated by Expert Code Review (auto) for issue #707 · ● 10.7M
| delete window.__recentlySentIds[id]; | ||
| if (desired && !current) continue; // Don't restore stale text into a cleared input |
There was a problem hiding this comment.
🟡 MODERATE — Flag consumed unconditionally; subsequent re-render can bypass guard
Flagged by: 2/3 reviewers
The delete at line 834 runs unconditionally whenever the flag is present, regardless of whether the continue at line 835 actually fires. When current is non-empty (user typed between send and first re-render), desired && !current is false — the continue is skipped but the flag is already consumed. A second rapid re-render with stale desired from a concurrent SafeRefreshAsync would then find no flag and could restore stale text.
Failing scenario: User sends → types immediately → first re-render consumes flag without blocking (current non-empty) → racing SafeRefreshAsync dispatches second restoreDraftsAndFocus with pre-Remove stale draft → no flag remains → stale text restored.
Suggested fix: Only delete the flag when it provides protection:
if (window.__recentlySentIds && window.__recentlySentIds[id]) {
if (desired && !current) {
delete window.__recentlySentIds[id];
continue;
}
// Keep the flag — a later re-render may still carry stale data
}| // Mark this input as recently cleared by a send — prevents stale draft restore | ||
| window.__recentlySentIds = window.__recentlySentIds || Object.create(null); | ||
| window.__recentlySentIds[elementId] = true; |
There was a problem hiding this comment.
🟢 MINOR — __recentlySentIds entries leak for IDs not in the drafts loop
Flagged by: 3/3 reviewers
clearElementValue sets __recentlySentIds[elementId] = true for all callers, including multi-agent inputs (ma-input-*, sidebar-ma-input-*) that are never tracked in draftBySession. After send, __liveDrafts[id] is also deleted here (line 424). On the next render, the for (var id in drafts) loop in restoreDraftsAndFocus never iterates these IDs, so the delete at line 834 never fires. Entries accumulate for the page lifetime.
Impact: Low — bounded by unique session sends per page load, values are trivial booleans. Not a functional bug.
Suggested fix: Add a cleanup sweep at the end of restoreDraftsAndFocus:
if (window.__recentlySentIds) {
for (var orphan in window.__recentlySentIds) {
if (!(orphan in drafts)) delete window.__recentlySentIds[orphan];
}
}Move the delete of __recentlySentIds[id] to only execute when the from being consumed prematurely on renders where the condition isn't true, leaving the textarea vulnerable to stale draft restore on the next re-render. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove delete from skip branch: flag now survives across multiple stale re-renders until C# draft clears or user types (closes the race window where render #2 restores stale text) - Store Date.now() instead of true, sweep entries >5s old at the top of restoreDraftsAndFocus to prevent unbounded growth from session switches Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔍 CLI Multi-Model Code Review — PR #707Methodology: 3 independent reviewers with adversarial consensus. Findings validated through cross-examination. Previous Auto-Review Findings Status
CLI Review Findings
Discarded: 1 finding (misleading comment text) — flagged by 1/3, but automatically resolved when Finding #1 is fixed. Finding 1 — 🟡 MODERATE: One-shot flag allows stale restore on multi-render (3/3)File: Problem: The Race trace:
Fix applied (70a2beb): Removed Finding 2 — 🟢 MINOR: Unbounded growth of
|
Fixes #571
When a message is sent,
clearElementValueclears the textarea and__liveDrafts. But on a Blazor re-render,restoreDraftsAndFocuscould overwrite the empty textarea with a stale draft fromdraftBySessionif the timing was wrong.Fix
Mark inputs as 'recently sent' via
__recentlySentIdswhenclearElementValueruns.restoreDraftsAndFocuschecks this flag and skips restoring non-empty drafts into recently-cleared inputs. The flag is one-shot (cleared after first skip) to allow future legitimate draft restores.Tests
All 3,510 tests pass.