Skip to content

refactor(frontend): centralize prompt-sync reconciliation invariant - #2665

Merged
lukemarsden merged 1 commit into
mainfrom
refactor/prompt-sync-reconcile
Jun 19, 2026
Merged

refactor(frontend): centralize prompt-sync reconciliation invariant#2665
lukemarsden merged 1 commit into
mainfrom
refactor/prompt-sync-reconcile

Conversation

@lukemarsden

Copy link
Copy Markdown
Collaborator

Follow-up to #2664. The dirty-flag bug we just fixed existed because one rule — "a pull must never clear the dirty flag; only a successful push does" — was implemented ad-hoc at three local↔backend merge sites and got it wrong in two. That's how a promoted interrupt showed live in the UI while the backend kept interrupt=false: a status poll re-confirmed the entry "synced", so the push skipped it.

Change

Extract a single exported reconcileEntry(local, backend, pushed) that encodes the invariant once:

  • backend-owned fields (status/retry/error) always reflect the backend
  • syncedToBackend is cleared (true) only when pushed=true (a real push-ack); a pull (pushed=false) preserves a pending local edit (false stays false)

All three sites — mergeWithBackend, the syncToBackend push-ack, and the status poll — route through it, so the rule can't diverge again. The push-ack site is also now stricter: it clears the dirty flag only for the entries it actually pushed (toSync), not every entry in the response.

Tests

usePromptHistory.test.ts covers the invariant directly — pull-preserves-dirty (the bug), push-clears-dirty, backend status reflected even on a dirty entry, frontend-owned fields (interrupt/content) not clobbered. These would have caught the original bug. Locally: 12/12 prompt tests green, yarn build clean.

See design/2026-06-19-incident-interrupt-during-boot-context-loss.md.

🤖 Generated with Claude Code

The "a pull must never clear the dirty flag; only a successful push does" rule
was implemented ad-hoc at three local↔backend merge sites and got it wrong in
two — which is how a promoted interrupt showed as live in the UI while the
backend kept interrupt=false (the merge re-confirmed the entry "synced" on a
poll, so the push skipped it).

Extract a single exported `reconcileEntry(local, backend, pushed)` that encodes
the invariant once:
- backend-owned fields (status/retry/error) always reflect the backend
- syncedToBackend cleared (true) only when pushed=true (a real push-ack);
  a pull (pushed=false) preserves a pending local edit (false stays false)

All three sites — mergeWithBackend, the syncToBackend push-ack, and the status
poll — now route through it, so the invariant can't diverge again. The push-ack
site is also now more correct: it clears the dirty flag only for the entries it
actually pushed (toSync), not every entry in the response.

Adds usePromptHistory.test.ts covering the invariant (pull-preserves-dirty,
push-clears-dirty, backend status reflected, frontend-owned fields not
clobbered) — these would have caught the original bug.

Follow-up to #2664. See design/2026-06-19-incident-interrupt-during-boot-context-loss.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lukemarsden
lukemarsden merged commit 21c7bff into main Jun 19, 2026
5 checks passed
@lukemarsden
lukemarsden deleted the refactor/prompt-sync-reconcile branch June 19, 2026 13:55
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