Skip to content

fix: Prevent corrupting restored order state on relaunch#589

Open
BraCR10 wants to merge 2 commits into
mainfrom
fix/restore-state-lost-after-relaunch
Open

fix: Prevent corrupting restored order state on relaunch#589
BraCR10 wants to merge 2 commits into
mainfrom
fix/restore-state-lost-after-relaunch

Conversation

@BraCR10
Copy link
Copy Markdown
Member

@BraCR10 BraCR10 commented May 7, 2026

Closes #584

Summary

  • After restore, MostroService._onData was saving relay-replayed events to mostroStorage with DateTime.now() timestamps. Those were newer than the synthetic restore messages (which use orderDetail.createdAt). On relaunch, sync() replayed them last, overwriting the correct restored state.
  • Fix: skip addMessage in _onData when isRestoringProvider is true. Event IDs still register in eventStorage for dedup.

How to test

  1. Restore account with active orders/disputes
  2. Confirm orders show correctly after restore
  3. Force-kill → relaunch the app
  4. Orders should show correct state without manual refresh

Summary by CodeRabbit

  • Bug Fixes
    • Fixed a critical issue where direct messages could be incorrectly persisted to storage during restore operations. The system now properly prevents message writes while a restore is in progress, ensuring restore workflows complete cleanly without introducing data conflicts, duplication, or other inconsistencies.

…restore

During account restoration, live direct messages (DMs) were being written to local storage. This could interfere with the restoration process, potentially leading to an inconsistent or corrupted state where restored data is overwritten by new events, causing issues upon app relaunch. This change prevents new DMs from being saved while a restoration is active, ensuring the integrity of the restored account data.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Warning

Rate limit exceeded

@BraCR10 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 56 minutes and 34 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a47672ab-8c35-4f64-b36f-7caa29c72df3

📥 Commits

Reviewing files that changed from the base of the PR and between 5c0f3f6 and 80398e4.

📒 Files selected for processing (2)
  • docs/architecture/DISPUTE_CHAT_RESTORE.md
  • docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md

Walkthrough

MostroService adds a guard that checks isRestoringProvider before persisting incoming decrypted DM messages to storage. When a restore is in progress, the service skips writing messages, logging the reason and returning early.

Changes

Restore Guard for Message Persistence

Layer / File(s) Summary
Dependency
lib/services/mostro_service.dart (line 18)
Import restore_mode_provider.dart to access the restore state flag.
Message Persistence Guard
lib/services/mostro_service.dart (lines 166–171)
In _onData, check if isRestoringProvider is true; if so, log and return early before calling messageStorage.addMessage(...), preventing writes during restore flows.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Suggested reviewers

  • Catrya
  • grunch

Poem

🐰 A restore in bloom, yet messages bloom too—
We skip their storage when mending anew,
No writes shall corrupt what restore must renew,
The guard stands steady: "Not now, wait for you!"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: Prevent corrupting restored order state on relaunch' clearly and specifically summarizes the main change: preventing order state corruption on app relaunch after restore.
Linked Issues check ✅ Passed The PR addresses issue #584 by implementing the fix to prevent live messages from overwriting restored state, directly resolving the root cause of orders showing wrong state after relaunch.
Out of Scope Changes check ✅ Passed All changes are narrowly scoped to the fix: importing restore_mode_provider and adding a guard in MostroService._onData to skip message persistence during restore, directly addressing the linked issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/restore-state-lost-after-relaunch

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/services/mostro_service.dart (1)

115-118: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Live events arriving during restore are permanently dropped from mostroStorage

The event ID is committed to eventStore unconditionally at lines 115–118, before the restore guard at lines 166–169. When isRestoringProvider is true, the guard returns early without writing to mostroStorage. After restore completes and isRestoringProvider transitions to false, the eventStore retains those cached IDs indefinitely. If the relay replays or resends those events, eventStore.hasItem(event.id!) at line 112 returns true and the handler exits before reaching the guard — the message is never stored, regardless of relay retries.

Any genuinely live event (e.g. a counterparty payment confirmation arriving while restore was running) is silently and permanently excluded from mostroStorage. The restore process does not invalidate eventStorageProvider or trigger a re-subscription with since filters to recover missed events.

A minimal mitigation is to defer the event ID write until after the restore guard, so the event remains eligible for normal processing once isRestoringProvider transitions to false:

🔧 Proposed fix
  Future<void> _onData(NostrEvent event) async {
    final eventStore = ref.read(eventStorageProvider);

    if (await eventStore.hasItem(event.id!)) return;

-   // Reserve event ID immediately to prevent duplicate processing from multiple relays
-   await eventStore.putItem(event.id!, {
-     'id': event.id,
-     'created_at': event.createdAt!.millisecondsSinceEpoch ~/ 1000,
-   });

    // ... (decryption, JSON parsing, restore-payload filters) ...

+   final isRestoring = ref.read(isRestoringProvider);
+
+   if (isRestoring) {
+     logger.i('Restore in progress, skipping storage write for ${msg.action}');
+     return;
+   }
+
+   // Reserve event ID to prevent duplicate processing from multiple relays.
+   // Deferred until after restore guard to allow post-restore reprocessing.
+   await eventStore.putItem(event.id!, {
+     'id': event.id,
+     'created_at': event.createdAt!.millisecondsSinceEpoch ~/ 1000,
+   });

-   if (ref.read(isRestoringProvider)) {
-     logger.i('Restore in progress, skipping storage write for ${msg.action}');
-     return;
-   }

    await messageStorage.addMessage(messageKey, msg);

This ensures that events received during restore are not marked as processed in eventStore, allowing them to be correctly stored when they arrive again post-restore.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/mostro_service.dart` around lines 115 - 118, The event ID is
being committed to eventStore via eventStore.putItem before the
isRestoringProvider guard, causing live events received during restore to be
marked processed and permanently skipped; move the eventStore.putItem call so it
runs only after the isRestoringProvider check and after the event has been
successfully written to mostroStorage (i.e., only write the ID in the branch
where you call mostroStorage write/success), ensuring
eventStore.hasItem(event.id!) remains false during restore and the event can be
processed/replayed later once isRestoringProvider is false.
🧹 Nitpick comments (1)
lib/services/mostro_service.dart (1)

156-169: 💤 Low value

Minor inefficiency: JSON parsing before the restore guard

MostroMessage.fromJson(result[0]) at line 156 is invoked before the isRestoringProvider check at line 166. During high-volume restore replay, every incoming event pays the full deserialization cost even though the result is discarded. Moving the guard to just after the restore-payload filter (line 154) avoids this work.

♻️ Proposed refactor
+      final isRestoring = ref.read(isRestoringProvider);
+      if (isRestoring) {
+        logger.i('Restore in progress, skipping storage write');
+        return;
+      }
+
       final msg = MostroMessage.fromJson(result[0]);
       final messageStorage = ref.read(mostroStorageProvider);
       final messageKey = decryptedEvent.id ?? event.id ?? 'msg_${DateTime.now().millisecondsSinceEpoch}';
-      if (ref.read(isRestoringProvider)) {
-        logger.i('Restore in progress, skipping storage write for ${msg.action}');
-        return;
-      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/services/mostro_service.dart` around lines 156 - 169, Move the
isRestoringProvider guard so we skip expensive JSON deserialization when
restoring: check ref.read(isRestoringProvider) (and the existing payload filter
at the top of the handler) before calling MostroMessage.fromJson(result[0]). In
practice, relocate the restore-check above the current MostroMessage.fromJson
call and only construct msg and compute messageKey (using
decryptedEvent.id/event) after the guard passes; preserve the existing
logger.i('Restore in progress...') early-return behavior and keep references to
ref.read(mostroStorageProvider), decryptedEvent, event, and messageKey intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@lib/services/mostro_service.dart`:
- Around line 115-118: The event ID is being committed to eventStore via
eventStore.putItem before the isRestoringProvider guard, causing live events
received during restore to be marked processed and permanently skipped; move the
eventStore.putItem call so it runs only after the isRestoringProvider check and
after the event has been successfully written to mostroStorage (i.e., only write
the ID in the branch where you call mostroStorage write/success), ensuring
eventStore.hasItem(event.id!) remains false during restore and the event can be
processed/replayed later once isRestoringProvider is false.

---

Nitpick comments:
In `@lib/services/mostro_service.dart`:
- Around line 156-169: Move the isRestoringProvider guard so we skip expensive
JSON deserialization when restoring: check ref.read(isRestoringProvider) (and
the existing payload filter at the top of the handler) before calling
MostroMessage.fromJson(result[0]). In practice, relocate the restore-check above
the current MostroMessage.fromJson call and only construct msg and compute
messageKey (using decryptedEvent.id/event) after the guard passes; preserve the
existing logger.i('Restore in progress...') early-return behavior and keep
references to ref.read(mostroStorageProvider), decryptedEvent, event, and
messageKey intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 363e3505-e6a5-4e82-ad3b-1de7259320e3

📥 Commits

Reviewing files that changed from the base of the PR and between b8bbee9 and 5c0f3f6.

📒 Files selected for processing (1)
  • lib/services/mostro_service.dart

During account restoration, `MostroService` saved historical relay events with current timestamps. This caused them to be replayed after the authoritative synthetic messages (which use original creation timestamps), leading to incorrect order state after an app relaunch. The `isRestoringProvider` flag now prevents `addMessage` during restore, ensuring only the correct restored state persists.
@BraCR10 BraCR10 requested a review from Catrya May 7, 2026 16:03
Copy link
Copy Markdown
Contributor

@mostronatorcoder mostronatorcoder Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the direction of the fix, but there is still a real correctness issue in the current implementation that makes this unsafe to merge as-is.

The problem is the interaction between the new restore guard and event deduplication in MostroService._onData:

  • event IDs are still registered in eventStorage before the restore guard returns
  • when isRestoringProvider is true, the message is not written to mostroStorage
  • after restore finishes, that event is now considered already seen and will not be reprocessed

So any genuinely live DM that arrives during the restore window can be permanently dropped from mostroStorage. This fixes one corruption mode, but introduces another one: missing live events that happened during restore.

That tradeoff is not safe unless the restore flow explicitly guarantees that every event seen during restore is either:

  1. replayed again after restore, or
  2. intentionally buffered and persisted later, or
  3. guaranteed to be fully represented by the synthetic restore state.

From the code in this PR, I do not see that guarantee. Right now the implementation looks like: dedup yes, persist no, replay no. That can lose data.

Please address that gap before merge, either by moving the restore guard earlier so skipped events are not marked as processed, or by buffering/replaying skipped live events once restore completes, or by documenting and enforcing a stronger invariant that makes the drop impossible in practice.

@BraCR10
Copy link
Copy Markdown
Member Author

BraCR10 commented May 14, 2026

I agree with the direction of the fix, but there is still a real correctness issue in the current implementation that makes this unsafe to merge as-is.

The problem is the interaction between the new restore guard and event deduplication in MostroService._onData:

  • event IDs are still registered in eventStorage before the restore guard returns
  • when isRestoringProvider is true, the message is not written to mostroStorage
  • after restore finishes, that event is now considered already seen and will not be reprocessed

So any genuinely live DM that arrives during the restore window can be permanently dropped from mostroStorage. This fixes one corruption mode, but introduces another one: missing live events that happened during restore.

That tradeoff is not safe unless the restore flow explicitly guarantees that every event seen during restore is either:

  1. replayed again after restore, or
  2. intentionally buffered and persisted later, or
  3. guaranteed to be fully represented by the synthetic restore state.

From the code in this PR, I do not see that guarantee. Right now the implementation looks like: dedup yes, persist no, replay no. That can lose data.

Please address that gap before merge, either by moving the restore guard earlier so skipped events are not marked as processed, or by buffering/replaying skipped live events once restore completes, or by documenting and enforcing a stronger invariant that makes the drop impossible in practice.

I understand the concern. The filter exists because Nostr relays sometimes replay old events with newer timestamps, which caused sync() to end up on a stale state in earlier versions. The risk of losing a genuine live event in the 10 second window is low, but acknowledged. The user can always refresh to recover. I consider it worth a low-priority follow-up issue
but the current implementation solves the issue of wrong state after relaunch the app.

@mostronatorcoder
Copy link
Copy Markdown
Contributor

Thanks for the clarification. I agree that this patch fixes the specific relaunch corruption path you were targeting, and I also agree the probability of a genuinely live DM landing inside that restore window is likely low in practice.

That said, I do not think the reply fully resolves the correctness concern I raised. The current behavior is still: an event can be marked as seen in eventStorage, skipped from mostroStorage, and then never replayed. Manual refresh is a recovery path for the user, but it is not the same as proving the implementation is loss-free during restore.

So my view is:

  • the PR definitely fixes one real bug
  • the introduced tradeoff may be acceptable as an explicit product/engineering decision
  • but it is still a tradeoff, not a full technical resolution of the data-loss concern

Because of that, I am not going to claim my original objection was wrong. If the team decides the low-probability drop during the restore window is acceptable compared with the relaunch corruption it prevents, that is a project call. But I think it should be acknowledged as such, ideally with a follow-up issue tracking either buffering, replay, or moving the dedup boundary.

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.

Order and dispute state lost after closing the app

1 participant