Skip to content

fix(chat): reconcile and dismiss browser-local unsent submissions - #1970

Merged
pancacake merged 1 commit into
HKUDS:devfrom
MrmoLabs:codex/failed-submission-recovery
Oct 18, 2026
Merged

pancacake merged 1 commit into
HKUDS:devfrom
MrmoLabs:codex/failed-submission-recovery

Conversation

@MrmoLabs

Copy link
Copy Markdown

Problem

Browser-local failed submissions can remain appended to an otherwise valid server conversation, with no way to dismiss individual records from the transcript. In the reported case, a regular Chrome profile showed two identical-looking Not sent user bubbles, while an embedded browser opening the same session showed only server history. The two recovered bubbles had distinct temporary message IDs: they were separate pending submissions, rather than evidence of duplicate server rows.

Pending submissions are stored in localStorage, with sessionStorage and legacy-format fallbacks. Browser profiles have separate storage, and clearing the HTTP cache does not clear these recovery records.

Several gaps made this confusing and persistent:

  • Foreground session loads restored and reconciled pending submissions, but background revalidation skipped that work. A refresh could hide pending messages without clearing their records, so they reappeared after a full reload.
  • The shared user-message UI displayed Not sent without a per-record discard action or an inline retry for the eligible unsent tail.
  • Successful session deletion did not clear the corresponding browser recovery records.
  • Treating the restored transcript as failed also obscured the underlying server status, which must be restored when the last local pending message is discarded.

Reproduction

  1. Start with an existing conversation containing a completed exchange.
  2. Disconnect the browser before a new submission reaches the server. Submit a short request such as “Create an outline” and wait for connection retries to fail.
  3. Submit the same text again after the first failure settles.
  4. Reconnect and reload the session. Both pending records are recovered in that browser.
  5. Open the same session in a browser profile with no recovery records: only server history appears.
  6. Clear only the original browser's HTTP cache and reload: its recovery records still appear.
  7. Trigger background session revalidation and then reload to observe the previous inconsistency between those two load paths.

The fix provides explicit recovery actions and consistent reconciliation; browser-local drafts still belong to their originating browser profile.

Changes

  • Reconcile browser recovery records on both foreground loads and background revalidation.
  • Clear records whose client_submission_id is present on a persisted server user row, including when opening an active remote turn.
  • Preserve pending messages on background refresh, including in-memory messages that could not be written to either browser storage.
  • Add an always-visible Discard action to each local unsent bubble and Retry to the eligible visible tail.
  • Wire the shared recovery actions through Chat, Mastery, and Reading surfaces. Reading also receives the existing retry-state wiring.
  • Prevent unsent rows from exposing the persisted-message Edit action.
  • Discard only the selected local negative-ID user row. Reparent its successors and update local branch selections so later pending messages remain visible.
  • Preserve the underlying runtime status so discarding the last pending message does not incorrectly make a completed server reply retryable or hide a genuine server failure.
  • Clear recovery data for the exact deleted session only after successful HTTP/JSON deletion confirmation. Failed requests and responses with deleted: false preserve recovery data.

Data preservation

Equal text is not sufficient proof of delivery. Distinct submission IDs continue to be treated independently, even if their contents are identical. Delivery reconciliation uses client_submission_id, and retry continues to use the existing request snapshot and submission identity.

Discard is limited to local failed submissions and is blocked while streaming. It does not call the server message-deletion endpoint. Existing per-record tombstones also prevent discarded legacy records or fallback copies from reappearing. Session cleanup preserves other sessions' records and clears any associated draft-source binding.

Validation

  • 57 tests passed across:
    • chat-failed-submission-recovery.spec.tsx
    • integration/chat-messages.spec.tsx
    • session-failed-submission-cleanup.spec.ts
  • 18 existing Node tests passed for resend decisions, resend wiring, and session-load failure handling.
  • TypeScript typecheck passed.
  • Dependency architecture check passed (912 modules, no violations).
  • ESLint on changed files passed with zero errors; two existing img/alt warnings remain in ChatWorkspace.
  • Translation-key parity and repository hygiene checks passed.
  • git diff --check passed.
  • Browser inspection on the affected Mastery session confirmed Discard on both recovered rows and Retry on the eligible tail. No live LLM request or deletion of the user's pending messages was used for this check.
  • The fix was also applied to the local running checkout; the 57 relevant tests and typecheck passed there again.

The changes add 16 regression cases covering individual disposal of identical pending submissions, legacy tombstones, draft recovery, selected-path continuity, storage failure, streaming guards, causal acceptance, runtime status restoration, and session-scoped deletion cleanup.

Not run: production build and the complete pre-commit suite (pre-commit is not installed in this environment).

Scope

This PR addresses browser-local unsent-message recovery and its lifecycle. It does not claim to resolve the separately reported issue where deleting conversation A appears to affect conversation B; that requires its own reproduction and investigation.

Modules affected

  • web (Frontend)
  • tests

Checklist

  • This PR targets dev.
  • I have read and followed the contribution guidelines.
  • Relevant regression tests have been added and passed.
  • Typecheck, targeted lint, architecture, translation parity, and repository hygiene checks passed.
  • Complete pre-commit run --all-files (not installed; see validation above).
  • Recovery actions preserve submission identity and restrict disposal to local unsent rows.

@pancacake
pancacake merged commit 4688fbf into HKUDS:dev Oct 18, 2026
15 checks passed
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.

2 participants