Skip to content

fix: Prevent corrupting restored order state on relaunch#589

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

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

Conversation

@BraCR10

@BraCR10 BraCR10 commented May 7, 2026

Copy link
Copy Markdown
Member

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
    • During restore, incoming DM relay events are buffered and not persisted until restore completes, then replayed to avoid ordering-related state loss.
    • Orders subscriptions now apply limit: 0 while restoring to prevent historical relay events from being processed in the restore window.
    • Restored dispute/message timestamps and storage keys are now consistently anchored to the restore run start time.
  • Documentation
    • Updated restore-mode and dispute/restore architecture documentation to describe buffering, deduplication, and correct restored-state handling.

…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

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Restore mode now limits orders backfill, buffers live relay events in MostroService, anchors restore timestamps, flushes buffered events when restore ends, and updates the restore architecture docs.

Changes

Restore-mode message handling

Layer / File(s) Summary
Orders subscription limit during restore
lib/features/subscriptions/subscription_manager.dart
Imports the restore-mode provider and sets the orders filter limit to 0 while restoring, otherwise null.
Restore run timestamp anchoring
lib/features/restore/restore_manager.dart
Stores a restore-run start timestamp and reuses it for restored dispute and order message timestamps and storage keys.
MostroService restore buffering and flush
lib/services/mostro_service.dart
Adds restore buffer state, listens for restore completion, buffers incoming events during restore, and flushes buffered events after restore ends.
Restore architecture documentation updates
docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md, docs/architecture/DISPUTE_CHAT_RESTORE.md
Updates the restore-mode documentation to describe the new buffering flow, the restore-time subscription limit, and the fixed dispute restore issue.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • Catrya
  • AndreaDiazCorreia

Poem

🐇 I hop through restore with a buffer in tow,
Live crumbs stay tucked till the right winds blow.
When the flag flips low and the replay is done,
I bounce them back clean, one by one.

🚥 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 clearly summarizes the main fix: preventing restored order state from being corrupted after relaunch.
Linked Issues check ✅ Passed The changes address #584 by preserving restore-time state, buffering live events, and keeping replay order stable after relaunch.
Out of Scope Changes check ✅ Passed The code and doc updates stay focused on restore-state preservation and related event handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/restore-state-lost-after-relaunch

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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

@mostronatorcoder mostronatorcoder Bot left a comment

Copy link
Copy Markdown
Contributor

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

BraCR10 commented May 14, 2026

Copy link
Copy Markdown
Member Author

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.

@ermeme ermeme Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review Summary

Verdict: Changes Requested

🔴 Critical

  • lib/services/mostro_service.dart — the new if (ref.read(isRestoringProvider)) return; guard is too broad. It skips all incoming DMs while restore is active, but eventStorage.putItem(...) already ran, so those events are permanently marked as seen and will not be replayed later. If a real live message arrives during the restore window, it is silently lost.

⚠️ Warning

  • The fix still relies on a 10-second restore window. Even if the intention is only to block historical relay replay, the current implementation cannot distinguish replayed messages from legitimate live messages. This can reintroduce the same class of corruption or cause message loss.

What to change

  • Scope the protection to restore-generated/historical payloads only, or buffer incoming messages during restore and process them after the restore completes. Do not drop them after reserving the event ID.

✅ Good

  • The root-cause docs were updated consistently.
  • Keeping event IDs in eventStorage for deduplication is correct in principle; the issue is the timing of the storage reservation relative to the restore gate.

@BraCR10

BraCR10 commented May 23, 2026

Copy link
Copy Markdown
Member Author

Hello @Catrya @grunch what do you think? Should this PR include logic to avoid losing events during the 10-second restore window, or is that better handled as a follow-up?

@Catrya

Catrya commented Jun 8, 2026

Copy link
Copy Markdown
Member

Hey @BraCR10, sorry for the delay. I think Mostroneitor has a valid point: a user can restore their account in the middle of an order and, right within those ~10 seconds, receive a status change or another important DM, so I think we should account for that.

The catch is that the event ID gets reserved in eventStorage (the putItem) before the isRestoring check, so the event is marked as processed but never stored → on the next launch hasItem discards it and it's never reprocessed. So it's dedup yes, persist no, replay no = the message is lost.

I'd suggest buffer-and-replay, which seems cleanest:

  • During restore, instead of dropping the live event, push it into a queue (_restoreBuffer).
  • When restore finishes (isRestoring flips to false), go through the buffer and process each event normally.
  • Easiest way is to extract the processing logic from _onData into a _processEvent() method and trigger the flush with a ref.listen(isRestoringProvider) inside MostroService, so it covers both the success and error paths without coupling restore_manager to the buffer.

That way we don't break the dedup contract and no message gets lost. As a bonus, it also fixes another gap: a live event arriving before its session is recreated is currently lost too, and with the buffer it'll match since the session exists by flush time.
Since it touches the same lines as this PR, I'd put it right here instead of as a follow-up. What do you think?

BraCR10 added 2 commits June 10, 2026 15:48
During account restoration, live Nostr events could interfere with the integrity of the restored state, leading to inconsistencies or potential state loss.

This change introduces a mechanism to:
- Buffer all incoming live events via `MostroService` while restoration is active, processing them only after restoration completes.
- Temporarily halt live order subscriptions (`limit: 0`) to prevent new order events from conflicting with historical data.

This ensures a clean restoration process, prevents state corruption, and guarantees that all live events are eventually processed without loss or interference.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
lib/features/subscriptions/subscription_manager.dart (1)

42-47: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Orders subscription is not refreshed when restore mode flips back to false.

limit is computed from ref.read(isRestoringProvider) at filter-build time (Line 132), but subscriptions are rebuilt from session changes only. If sessions are unchanged on restore completion, the orders subscription keeps restore-time filter settings.

Also applies to: 122-133

🤖 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/features/subscriptions/subscription_manager.dart` around lines 42 - 47,
The orders subscription keeps restore-time filters because limit is computed
only when filters are built and subscriptions are only rebuilt on session
changes; add a listener for the restore flag so subscriptions are rebuilt when
restore mode flips. In _initSessionListener add a
ref.listen<bool>(isRestoringProvider, ...) that calls
_updateAllSubscriptions(ref.read(sessionNotifierProvider)) (or otherwise
triggers the same rebuild path), and ensure _updateAllSubscriptions (and the
filter-building code that computes limit) reads ref.read/isRestoringProvider at
rebuild time rather than capturing it once earlier.
lib/services/mostro_service.dart (1)

120-127: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Buffered events are effectively dropped due to dedupe short-circuit.

Line 123 reserves the event ID before the restore gate, then _flushRestoreBuffer() replays with _onData (Line 197). On replay, Line 120 returns immediately (hasItem == true), so buffered live events never reach addMessage.

Also applies to: 174-177, 191-198

🤖 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 120 - 127, The dedupe
short-circuit causes buffered events replayed by _flushRestoreBuffer -> _onData
to be dropped because eventStore.putItem is called before the restore/replay
completes; change the flow so reservation does not block replay: either move the
eventStore.putItem call to after the restore/restore-buffer gate (i.e., only
reserve the ID once _flushRestoreBuffer has replayed buffered events), or add a
replay flag/state (e.g., _isRestoring or pass isReplay into _onData) and modify
the early-return that uses eventStore.hasItem to ignore dedupe when replaying;
update references to eventStore.hasItem, eventStore.putItem,
_flushRestoreBuffer, _onData, and addMessage accordingly so buffered events
reach addMessage during replay.
🤖 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.

Inline comments:
In `@lib/services/mostro_service.dart`:
- Around line 50-55: init() is attaching a new ref.listen to isRestoringProvider
every time, causing duplicate listeners because dispose() only cancels
_ordersSubscription; fix by storing the listener handle when calling ref.listen
(e.g., assign the returned subscription to a field like _restoreListener or
similar) and ensure you cancel/close it in dispose(), and also cancel any
existing _restoreListener before re-assigning in init() so you never accumulate
multiple ref.listen callbacks that call _flushRestoreBuffer().

---

Outside diff comments:
In `@lib/features/subscriptions/subscription_manager.dart`:
- Around line 42-47: The orders subscription keeps restore-time filters because
limit is computed only when filters are built and subscriptions are only rebuilt
on session changes; add a listener for the restore flag so subscriptions are
rebuilt when restore mode flips. In _initSessionListener add a
ref.listen<bool>(isRestoringProvider, ...) that calls
_updateAllSubscriptions(ref.read(sessionNotifierProvider)) (or otherwise
triggers the same rebuild path), and ensure _updateAllSubscriptions (and the
filter-building code that computes limit) reads ref.read/isRestoringProvider at
rebuild time rather than capturing it once earlier.

In `@lib/services/mostro_service.dart`:
- Around line 120-127: The dedupe short-circuit causes buffered events replayed
by _flushRestoreBuffer -> _onData to be dropped because eventStore.putItem is
called before the restore/replay completes; change the flow so reservation does
not block replay: either move the eventStore.putItem call to after the
restore/restore-buffer gate (i.e., only reserve the ID once _flushRestoreBuffer
has replayed buffered events), or add a replay flag/state (e.g., _isRestoring or
pass isReplay into _onData) and modify the early-return that uses
eventStore.hasItem to ignore dedupe when replaying; update references to
eventStore.hasItem, eventStore.putItem, _flushRestoreBuffer, _onData, and
addMessage accordingly so buffered events reach addMessage during replay.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 622889c4-d343-4b54-80df-6d0a732f9d51

📥 Commits

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

📒 Files selected for processing (4)
  • docs/architecture/DISPUTE_CHAT_RESTORE.md
  • docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md
  • lib/features/subscriptions/subscription_manager.dart
  • lib/services/mostro_service.dart
✅ Files skipped from review due to trivial changes (1)
  • docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md

Comment thread lib/services/mostro_service.dart

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to prevent restored order/dispute state from being overwritten on app relaunch by avoiding persistence of relay-replayed events during the restore window, so that sync() replays only the authoritative synthetic restore messages.

Changes:

  • Add restore-mode gating in MostroService._onData to avoid writing messages to mostroStorage during restore (and introduce buffering/flush logic).
  • Adjust orders subscription filter during restore (SubscriptionManager) to reduce/avoid historical replay.
  • Update architecture documentation to describe restore-mode protections and mark the dispute restore/relaunch issue as fixed.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
lib/services/mostro_service.dart Adds restore-mode checks around DM processing/storage and introduces a restore buffer + flush mechanism.
lib/features/subscriptions/subscription_manager.dart Modifies orders subscription filter to apply a restore-dependent limit.
docs/architecture/SESSION_RECOVERY_ARCHITECTURE.md Documents restore-mode protections and intended effects on message persistence/state reconstruction.
docs/architecture/DISPUTE_CHAT_RESTORE.md Updates root-cause + fix narrative and marks the relaunch-after-restore issue as fixed.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/services/mostro_service.dart
Comment thread lib/services/mostro_service.dart
Comment on lines 129 to 133
return NostrFilter(
kinds: [1059],
p: sessions.map((s) => s.tradeKey.public).toList(),
limit: ref.read(isRestoringProvider) ? 0 : null,
);
BraCR10 added 2 commits June 10, 2026 18:07
Update the documentation for "Issue 3 — Dispute State Not Persisted After Restore + App Kill" in `DISPUTE_CHAT_RESTORE.md`.

This revision clarifies the two-part fix for robust account restoration:
1. `SubscriptionManager` now filters historical order events by passing `limit: 0` to relays during restore.
2. `MostroService._onData` buffers any incoming live events, applying them only after the restoration process has completed.

This ensures a clean and consistent state on app relaunch by preventing interference from live or historical events during the restoration window.
…restore

This refines the account restoration process by formalizing the management of incoming Nostr events and subscription states.

Dedicated listeners in `SubscriptionManager` and `MostroService` now precisely control:
- When subscriptions request `limit:0` to block historical backfill during restore.
- When live events are buffered by `MostroService`.
- When the buffer is flushed and subscriptions resume normal operation with `limit:null` after restoration completes.

This ensures a clean, consistent state by preventing interference from live or historical events with the authoritative synthetic state built during restore.

@mostronatorcoder mostronatorcoder Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I re-reviewed the current head and I still see one real correctness issue that I would block on.

Blocking issue:

  • SubscriptionManager now computes the orders filter with limit: ref.read(isRestoringProvider) ? 0 : null, but that value is captured when the filter is built.
  • From the changes in this PR, I do not see a corresponding subscription refresh triggered when restore mode flips from true back to false.
  • If the session set is unchanged when restore completes, the existing orders subscription can remain alive with the restore-time limit: 0 filter instead of being rebuilt into the normal runtime filter.

Why this matters:

  • The PR’s safety story depends on restore mode being temporary: block historical replay during restore, then return to the normal live subscription shape afterwards.
  • If the orders subscription is not refreshed on restore exit, the app can stay in the restore-mode subscription shape longer than intended.
  • That makes the fix incomplete because the relay-side protection is no longer scoped cleanly to the restore window.

What I would want:

  • Rebuild or refresh the orders subscription when isRestoringProvider changes, not only when the session set changes.
  • In other words, restore-mode transition itself needs to be a first-class resubscription trigger.

The buffering work in MostroService is the right direction and does address the earlier data-loss issue, but this subscription-lifecycle gap is still a blocker for me.

BraCR10 and others added 3 commits June 26, 2026 09:35
The `_restoreModeListener` was responsible for updating subscriptions when the restoration process completed, specifically by rebuilding them without the `limit:0` constraint. This functionality has now been consolidated into the broader orchestration of event buffering and subscription transitions, making this dedicated listener redundant.
Resolve conflict in subscription_manager.dart: apply restore-mode
limit:0 to both transport variants (giftWrap kind 1059, nip44 kind 14)
from transport v2 Phase A.
…arsing

Standardizes timestamps for synthetic messages generated during account restoration by using a single `_restoreStartTime`. This prevents inconsistencies from dynamically generated timestamps in message keys and content.

Additionally, NIP-59 gift-wrapped events now use the `createdAt` of the inner, decrypted event as the message timestamp. This captures the true send time, as the outer event's `createdAt` is randomized for privacy. These changes improve overall state consistency, particularly during restoration.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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)

193-196: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Move restore buffering before eventStorage reservation.

Line 193 buffers after Lines 122-126 already stored the outer event id. When _flushRestoreBuffer() replays the event on Line 221, _onData() hits hasItem(event.id!) and returns, so every buffered live event is dropped instead of persisted.

Proposed fix
 class MostroService {
   final Ref ref;
 
   Settings _settings;
   StreamSubscription<NostrEvent>? _ordersSubscription;
   ProviderSubscription<bool>? _restoreListener;
   final List<NostrEvent> _restoreBuffer = [];
+  final Set<String> _restoreBufferedEventIds = <String>{};
@@
   Future<void> _onData(NostrEvent event) async {
+    final eventId = event.id;
+    if (eventId == null) {
+      logger.w('Received event without id, skipping');
+      return;
+    }
+
+    if (ref.read(isRestoringProvider)) {
+      if (_restoreBufferedEventIds.add(eventId)) {
+        _restoreBuffer.add(event);
+        logger.i('Restore: buffered live event $eventId');
+      }
+      return;
+    }
+
     final eventStore = ref.read(eventStorageProvider);
 
-    if (await eventStore.hasItem(event.id!)) return;
+    if (await eventStore.hasItem(eventId)) return;
@@
-    await eventStore.putItem(event.id!, {
-      'id': event.id,
+    await eventStore.putItem(eventId, {
+      'id': eventId,
       'created_at': event.createdAt!.millisecondsSinceEpoch ~/ 1000,
     });
@@
-      if (ref.read(isRestoringProvider)) {
-        _restoreBuffer.add(event);
-        logger.i('Restore: buffered live event ${event.id} for ${msg.action}');
-        return;
-      }
-
@@
   Future<void> _flushRestoreBuffer() async {
     if (_restoreBuffer.isEmpty) return;
     final buffer = List<NostrEvent>.from(_restoreBuffer);
     _restoreBuffer.clear();
+    _restoreBufferedEventIds.clear();

Also applies to: 215-222

🤖 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 193 - 196, The restore
buffering in MostroService::_onData is happening after the outer event has
already been reserved in eventStorage, so buffered live events get skipped when
_flushRestoreBuffer replays them. Move the restore check and
_restoreBuffer.add(event) ahead of the eventStorage hasItem/add reservation
path, and ensure the replayed event can be persisted normally instead of
returning early on hasItem(event.id!).
🤖 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.

Inline comments:
In `@lib/services/mostro_service.dart`:
- Around line 199-204: The timestamp fallback in MostroService’s message
handling should use the original event time for direct DMs instead of processing
time. Update the logic around msg.timestamp assignment so that when
innerCreatedAt is null (such as kind 14), it falls back to event.createdAt
before calling messageStorage.addMessage, keeping NIP-44 ordering consistent.
Use the existing MostroService event/message flow and the msg.timestamp ??=
assignment to locate the fix.

---

Outside diff comments:
In `@lib/services/mostro_service.dart`:
- Around line 193-196: The restore buffering in MostroService::_onData is
happening after the outer event has already been reserved in eventStorage, so
buffered live events get skipped when _flushRestoreBuffer replays them. Move the
restore check and _restoreBuffer.add(event) ahead of the eventStorage
hasItem/add reservation path, and ensure the replayed event can be persisted
normally instead of returning early on hasItem(event.id!).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8bf14479-40b5-4539-a0c6-7695a73f9ac2

📥 Commits

Reviewing files that changed from the base of the PR and between 2913404 and 38454f9.

📒 Files selected for processing (3)
  • lib/features/restore/restore_manager.dart
  • lib/features/subscriptions/subscription_manager.dart
  • lib/services/mostro_service.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/features/subscriptions/subscription_manager.dart

Comment on lines +199 to 204
// Use inner rumor's created_at (real send time) — outer gift wrap is
// NIP-59 randomized. Historical events have old inner timestamps (sort
// below synthetics); live events have recent inner timestamps (gate passes).
msg.timestamp ??= innerCreatedAt?.millisecondsSinceEpoch;

await messageStorage.addMessage(messageKey, msg);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant service and storage behavior
git ls-files lib/services/mostro_service.dart lib/**/message_storage.dart lib/**/storage*.dart lib/**/message*.dart

echo '--- mostro_service.dart outline ---'
ast-grep outline lib/services/mostro_service.dart --view expanded || true

echo '--- search for addMessage and timestamp fallback ---'
rg -n "addMessage\\(|timestamp .*DateTime\\.now|msg\\.timestamp|innerCreatedAt|kind == 14|createdAt" lib/services lib -g '*.dart' -A 3 -B 3

Repository: MostroP2P/mobile

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant section with line numbers once the file is located.
sed -n '160,240p' lib/services/mostro_service.dart

Repository: MostroP2P/mobile

Length of output: 2891


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the implementation of messageStorage.addMessage if present.
rg -n "class .*MessageStorage|Future<.*addMessage|void addMessage|DateTime\\.now\\(" lib -g '*.dart' -A 5 -B 5

Repository: MostroP2P/mobile

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the MostroMessage model and its timestamp parsing.
rg -n "class MostroMessage|MostroMessage\\.fromJson|timestamp" lib -g '*.dart' -A 6 -B 6 | sed -n '1,220p'

Repository: MostroP2P/mobile

Length of output: 16989


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the concrete storage implementation used by mostroStorageProvider and its addMessage fallback.
sed -n '1,220p' lib/shared/providers/storage_providers.dart
echo '---'
rg -n "class .*Storage|addMessage\\(|DateTime\\.now\\(" lib -g '*.dart' -A 8 -B 4 | sed -n '1,240p'

Repository: MostroP2P/mobile

Length of output: 18652


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the MostroMessage model and its timestamp field handling.
fd -a "mostro_message.dart" lib
fd -a "message.dart" lib | sed -n '1,80p'
rg -n "class MostroMessage|MostroMessage\\.fromJson|timestamp" lib -g '*.dart' -A 8 -B 6 | sed -n '1,220p'

Repository: MostroP2P/mobile

Length of output: 17321


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the concrete storage implementation behind mostroStorageProvider and its addMessage behavior.
rg -n "mostroStorageProvider|class .*Storage|addMessage\\(" lib -g '*.dart' -A 8 -B 6 | sed -n '1,260p'

Repository: MostroP2P/mobile

Length of output: 23071


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the MostroMessage model and the concrete MostroStorage implementation.
sed -n '1,220p' lib/data/models/mostro_message.dart
echo '---'
fd -a "mostro_storage.dart" lib
echo '---'
sed -n '1,260p' lib/data/repositories/mostro_storage.dart

Repository: MostroP2P/mobile

Length of output: 10636


Use event.createdAt for direct DMs. kind == 14 leaves innerCreatedAt null here, so MostroStorage.addMessage() falls back to DateTime.now() and reorders NIP-44 messages by processing time. Use event.createdAt as the fallback.

🤖 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 199 - 204, The timestamp
fallback in MostroService’s message handling should use the original event time
for direct DMs instead of processing time. Update the logic around msg.timestamp
assignment so that when innerCreatedAt is null (such as kind 14), it falls back
to event.createdAt before calling messageStorage.addMessage, keeping NIP-44
ordering consistent. Use the existing MostroService event/message flow and the
msg.timestamp ??= assignment to locate the fix.

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

3 participants