Skip to content

feat(#197): defer session removal so a trailing bond-slashed still decrypts - #258

Open
AndreaDiazCorreia wants to merge 9 commits into
mainfrom
feat/197-bond-slashed-session-defer
Open

feat(#197): defer session removal so a trailing bond-slashed still decrypts#258
AndreaDiazCorreia wants to merge 9 commits into
mainfrom
feat/197-bond-slashed-session-defer

Conversation

@AndreaDiazCorreia

@AndreaDiazCorreia AndreaDiazCorreia commented Jul 31, 2026

Copy link
Copy Markdown
Member

Refs #197 — deliberately not auto-closing it, see Scope below. Part of the
anti-abuse bond epic #145.

Problem

Cancel is where a session's life ends, and on a timeout slash the daemon sends
canceled first with bond-slashed ~150 ms behind it. v2 turned out never to
delete sessions at all — remove_session had no production caller — so the
policy had to be defined before #191 and #208 introduce that deletion, and the
leak closed with the deferral already in place.

Fix

  • SessionManager tracks deferred removals (defer / resolve / reconcile),
    with sessions and deferrals under one lock.
  • cancel_cleanup decides from the status held before the cancel: Pending
    removes at once (the cancel returns the bond), dispute and admin states keep
    the session (the admin chat still needs its keys), anything else — including
    an unknown status — defers 60 s.
  • Action::Canceled reads that status before syncing Canceled;
    Action::BondSlashed settles a pending deferral and is a no-op without one.
  • Both are gated on the recipient trade key, and deferrals carry the trade key
    index they were armed for, so a delivery from a superseded take cannot claim
    the session of the one that replaced it.
  • install_session: an accepted take replaces whatever it finds instead of
    discarding a SessionAlreadyExists error, which previously left a confirmed
    retake with no session at all.

Scope

This is session-cleanup policy, not a transport guarantee, and #197's
end-to-end acceptance criterion is not proven here — hence Refs, not
Closes.

v1's rationale for the grace period does not carry over: there, deleting the
session drops the trade key from the subscription filter and discards its
decryption key. In v2 the per-trade receiver captures its own trade keys and
ensure_global_dm_coverage retains them for the life of the process, so
reception and decryption never depend on the session. The window is a margin
for handling that needs session state, plus parity with v1's policy.

Exercising canceledbond-slashed end to end needs a receiver/dispatcher
regression and the taker bond flow from #208. Serializing the generation gate
with the order-book and DB writes is tracked in #259.

Divergence from v1

v1 keys off !userInitiated && hadBond; this keys off the pre-cancel status,
needing neither an outbound cancel marker nor a bond-policy lookup. It defers in
a few cases v1 would not — the cost is a session held 60 s longer. Sessions are
in-memory only, so v1's restart reconciliation does not apply. Documented in
specs/004-mostro-p2p-client/contracts/orders.md.

Tests

16 session tests plus 6 over the cancel-cleanup and generation helpers.
cargo test 256 passing, cargo clippy clean, flutter analyze clean,
flutter test 195 passing, frb-generate --check unchanged.

Summary by CodeRabbit

  • Bug Fixes

    • Improved order cancellation and bond-slashing handling to prevent stale events from affecting newer trades.
    • Added safer session cleanup based on order status, including deferred removal during bond grace periods.
    • Preserved sessions correctly when orders are retaken.
  • Documentation

    • Documented session lifecycle behavior, cancellation handling, retakes, and bond-grace-period rules.

…ssion loss

When retaking an order within the grace window, explicitly resolve any
pending deferred removal from the canceled take so the fresh session
created for the retake survives the old timer. create_session now clears
the deferral after inserting the new session, and take_order calls
resolve_deferred_removal before creating the session to ensure cleanup.
…ashed decryption

Adds Session lifecycle on cancel section explaining the 60-second deferral
that keeps the trade key in subscriptions so bond-slashed can be received
and decrypted. Documents the status-based removal rules (immediate for
Pending/Dispute, deferred otherwise), retake clearing the deferral, and
the deliberate divergence from v1's user-initiated/bond-policy approach.
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 85ce6347-3aa2-4802-9274-f3a86394ded9

📥 Commits

Reviewing files that changed from the base of the PR and between 55cbda5 and 0e3c2c0.

📒 Files selected for processing (1)
  • rust/src/api/orders.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • rust/src/api/orders.rs

Walkthrough

Cancellation now validates trade-key generations and applies status-based session cleanup. SessionManager tracks generation-bound deferred removals. Retakes and bond-slashed events resolve those removals. Accepted takes install sessions with error reporting. Tests and lifecycle contracts cover these paths.

Changes

Session cleanup lifecycle

Layer / File(s) Summary
Deferred cleanup policy and manager
rust/src/mostro/session.rs
Adds cleanup classification, generation-bound deferrals, replacement-safe session installation, early resolution, deadline reconciliation, scheduling, and focused tests.
Order cancellation and session flow
rust/src/api/orders.rs
Installs accepted-take sessions with error reporting, validates generations, applies cleanup after cancellation, and resolves deferrals for bond-slashed events.
Session lifecycle contract
specs/004-mostro-p2p-client/contracts/orders.md
Documents status synchronization, retake behavior, deferred cleanup, bond-slashed resolution, and generation checks.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CancellationHandler
  participant SessionManager
  participant DeferredRemovalScheduler
  participant BondSlashedHandler
  CancellationHandler->>SessionManager: validate current trade-key generation
  CancellationHandler->>SessionManager: apply status-based cleanup
  DeferredRemovalScheduler->>SessionManager: reconcile expired removal
  BondSlashedHandler->>SessionManager: resolve deferred removal
  CancellationHandler->>SessionManager: install accepted-take session
Loading

Possibly related issues

Poem

A rabbit checks each trade-key thread,
Then keeps old sessions safely shed.
A slash resolves deferred fate,
Retakes clear the obsolete gate.
Fresh sessions hop ahead.

🚥 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 identifies the main change: deferred session removal to allow a trailing bond-slashed notice to decrypt.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/197-bond-slashed-session-defer

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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e87ed32f6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rust/src/mostro/session.rs Outdated

@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

🤖 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 `@rust/src/mostro/session.rs`:
- Around line 127-134: Replace the separately guarded sessions and
deferred_removals maps with a shared SessionState protected by one RwLock, so
create_session atomically checks for stale sessions, inserts the new session,
and clears any deferred removal. Update resolve_deferred_removal and
reconcile_deferred_removals to atomically coordinate deadline removal with
session deletion, preserving existing behavior for due and unresolved entries.
Add a tokio::time::pause() test that interleaves reconciliation or resolution
with a concurrent retry and verifies no stale SessionAlreadyExists result.
🪄 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 Plus

Run ID: 715b2598-8f5d-4d92-b3df-e12212b1b481

📥 Commits

Reviewing files that changed from the base of the PR and between 2464ade and 1e87ed3.

📒 Files selected for processing (3)
  • rust/src/api/orders.rs
  • rust/src/mostro/session.rs
  • specs/004-mostro-p2p-client/contracts/orders.md

Comment thread rust/src/mostro/session.rs Outdated
@AndreaDiazCorreia

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa7441189a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rust/src/api/orders.rs Outdated

@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.

Blocking changes required

I reviewed the current head (aa7441189a) and revalidated the existing unresolved generation-binding thread. The shared SessionState lock fixes the previous two-map race, but cancellation cleanup is still correlated only by order_id.

On this exact head I reproduced the following sequence with a focused regression test:

  1. the old take has a deferred session;
  2. a retake creates a fresh session with a new trade_key_index;
  3. a delayed Canceled from the old trade key arms a new order-level deferral;
  4. the old BondSlashed resolves that deferral and removes the fresh session.

The added test fails at the assertion that the retaken session must survive. The same missing guard is also applied too late in the dispatcher: the Canceled arm removes the order-book entry and writes Canceled to the current DB trade before calling apply_cancel_cleanup. Therefore an old-generation delivery can corrupt the fresh retake's persisted/UI state even before its session is removed.

Please bind Canceled, BondSlashed, and deferred-removal state to the recipient trade_index (or another explicit session generation), reject generation mismatches before any order-book, DB, or session mutation, and add a dispatcher-level regression test covering delayed old-key Canceled/BondSlashed deliveries after a retake. This should extend the existing unresolved thread rather than merely adding another lock around order-ID state.

Verification on the current head:

  • cargo test: 249 passed, 8 ignored, 0 failed
  • Focused session/cancel tests: green
  • Temporary stale-generation regression probe: fails, because the fresh session is deleted
  • GitHub CI: Rust, Flutter, and web checks are green, but none exercises this event-ordering case

@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.

Blocking race still remains

I reviewed the latest commit (0aa219486d) as a delta from the previously reviewed head. The new generation-tagged deferral correctly prevents an already superseded old-key delivery from deleting the replacement session, and it fixes the prior timer/resolution behavior. However, it does not cover an old Canceled handler that passes the new check and is then overtaken by a retake while suspended in the following awaits.

That remaining interleaving can still corrupt the fresh order-book/DB state and can leave an accepted retake without any session; see the inline blocker. The new tests call generation helpers sequentially, so they cannot exercise this dispatcher/caller ordering.

Required before approval: make generation validation and the affected order-book/DB/session transition one coordinated per-order operation (or make every persisted/cache mutation generation-conditional), handle create_session failure instead of discarding it, and add a real interleaving test that pauses the old Canceled after validation, completes the retake, then resumes it.

Verification on the current head:

  • cargo test: 254 passed, 8 ignored, 0 failed
  • GitHub Rust, Flutter, and web checks: all green
  • Current branch and live base merge cleanly

Comment thread rust/src/api/orders.rs
…ted takes

Introduces install_session that unconditionally installs a session for a
daemon-confirmed take, replacing any stale session or clearing pending
deferrals. Unlike create_session which fails on collision, install_session
assumes the new take owns the order_id and logs a warning when replacing.
Refactors session construction into build_session helper shared by both
methods.
@AndreaDiazCorreia

Copy link
Copy Markdown
Member Author

Thanks — all four findings were the same defect seen at successively narrower
windows, and each round is in the branch:

  • Two-map race (Codex, CodeRabbit): sessions and deferred_removals now
    share one RwLock<SessionState>, so no operation observes one mid-update
    against the other.
  • Generation binding (Codex): Canceled and BondSlashed are gated on the
    recipient trade key before any mutation; deferrals carry the trade key index
    they were armed for, and neither the trailing notice nor the timer removes a
    session of another generation. Writing that exposed a real bug — the first
    version compared the generation against the deferral instead of against the
    session it was about to drop — which the new test caught.
  • TOCTOU after the gate (ermeme): the severe half is closed. take_order
    now uses install_session, which replaces whatever it finds and clears any
    pending deferral under the same lock, so an accepted take keeps its session
    whoever wins the race. The SessionAlreadyExists error is no longer
    discarded.

What is not in this PR: serializing the gate together with the order-book and DB
writes. That needs a per-order keyed mutex across the dispatcher and test
scaffolding to drive dispatch_mostro_message, and the Canceled arm has
mutated book and DB without any generation check since well before this branch.
Tracked separately in #259 rather than growing a PR about not losing a
bond-slashed.

Verified: cargo test 256 passing, cargo clippy clean, flutter analyze
clean, flutter test 195 passing, frb-generate --check unchanged.

@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.

Blocking scope/contract mismatch

I reviewed the full conversation and the current head (53b6c491fd). The latest install_session change fixes the session-loss half of the previously reported race: an accepted retake now replaces stale session state, and generation-bound deferred cleanup cannot delete that replacement. The remaining order-book/DB TOCTOU is pre-existing and is now explicitly tracked in #259, so I am not requiring this narrowly scoped PR to implement the dispatcher-wide lock.

However, the current PR still cannot be approved because its contract and closing metadata claim guarantees that this implementation does not establish:

  1. #197 requires the canceled -> bond-slashed sequence to be received and decrypted end-to-end, but none of the new tests invokes dispatch_mostro_message, the per-trade receiver, or the global receiver. a_delivery_to_a_superseded_trade_key_is_rejected only calls the generation helper twice.
  2. In the current v2 receive path, removing a Session does not take its trade key out of the relay filter or discard the decryption key. subscribe_gift_wraps captures recipient_keys in its spawned receiver, while ensure_global_dm_coverage retains freshly derived keys for the process lifetime. The documentation therefore imports a v1 premise that is not true for this implementation, and the pure SessionManager tests do not prove the stated transport invariant.
  3. The contract also says an old-key delivery is stopped before it can reach the order book or DB. That is contradicted by the acknowledged #259 interleaving: the generation read at orders.rs:1836 is released before the later awaits, so a retake can overtake it.

Please choose one coherent scope before merging:

  • Implement/verify the end-to-end guarantee: add a receiver/dispatcher-level regression that exercises Canceled followed by BondSlashed, and wire session lifetime to transport key/subscription lifetime if that coupling is actually intended; or
  • Make this preparatory policy only: remove the false transport and stale-delivery guarantees, correct the nonexistent process_gift_wrap_rumor reference, and stop auto-closing #197 until its end-to-end acceptance criterion can be exercised (likely with the bond flow from #208). Keep #259 as the separate dispatcher serialization work.

Verification:

  • Current head 53b6c491fd; clean checkout
  • cargo test --locked: 256 passed, 8 ignored, 0 failed
  • cargo clippy --locked: clean
  • Live main advanced to a149b8f160; merge is conflict-free and merged-tree cargo test --locked: 257 passed, 8 ignored, 0 failed
  • Exact-head GitHub Rust, Flutter, and web checks: green

Comment thread specs/004-mostro-p2p-client/contracts/orders.md Outdated
…rantee

Rewrites BOND_SLASH_GRACE_SECS comment and Session lifecycle on cancel
section to explain that unlike v1, the 60-second window does not make
bond-slashed arrive—per-trade receiver and ensure_global_dm_coverage
already retain trade keys for the process lifetime. The grace period is
a conservative margin for handling that needs session state, not a
transport dependency. Also renames process_gift_wrap_rumor references to
dispatch_mostro_message
ermeme[bot]
ermeme Bot previously approved these changes Aug 4, 2026

@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.

Approved — scope and contract are now aligned

I re-reviewed the current head (55cbda5454) against both the current PR description and the full text/conversation of #197. The previous scope blocker is resolved:

  • The PR now says Refs #197, not Closes #197; the issue remains open and its end-to-end receive/decrypt criterion will not be auto-closed by this merge.
  • The description and contract now state accurately that v2 transport keys/subscriptions outlive Session, so this PR is session-cleanup policy rather than a transport guarantee.
  • The nonexistent process_gift_wrap_rumor references were corrected to dispatch_mostro_message.
  • The remaining generation-check TOCTOU for order-book/DB writes is explicitly acknowledged and tracked in #259. It predates this session-cleanup policy and is no longer represented as solved here, so it is not a blocker for the clarified scope.

I also revalidated the previous session-specific blockers on the full current diff:

  • sessions and deferred removals share one lock;
  • deferred removal is bound to trade_key_index;
  • resolution/timer cleanup cannot delete a replacement generation;
  • an accepted retake uses install_session, clearing stale session/deferral state instead of silently losing its session.

No blocking issue remains for the PR as now described.

Verification:

  • Exact-head Rust, Flutter, and web GitHub checks: green
  • git diff --check: clean
  • Current head merges cleanly with live main (a149b8f160)
  • #197: open, with no closing keyword in the PR body

… and retake isolation

Adds daemon_message, slashed_bond_payload, and await_bond_slashed helpers.
Tests that canceled preserves session for trailing bond-slashed and that
canceled from a superseded trade key does not affect the retake's order
book entry or session.

@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.

Approved — the dispatcher regressions are meaningful

I reviewed the test-only delta from the previously approved head (55cbda5454..0e3c2c0e3). It does not change production behavior.

The two new tests cross the real dispatch_mostro_message boundary and cover the intended session-policy invariants:

  • Canceled leaves the session deferred, the trailing BondSlashed reaches the Dart-facing stream, and the deferral is settled.
  • A Canceled addressed to the superseded trade-key generation cannot remove the retake's order-book entry or affect its session.

These are mutation-sensitive for the relevant dispatcher wiring: removing the generation gate, deleting the session immediately, or failing to resolve the deferral would make the assertions fail. They deliberately start from an already-unwrapped message, so they do not alter the clarified scope: #197's receive/decrypt transport criterion remains open and is not auto-closed by this PR.

Non-blocking naming note: a_canceled_then_bond_slashed_sequence_is_handled_end_to_end and its doc comment would be more precise as ...through_the_dispatcher; the test constructs UnwrappedMessage directly and therefore does not exercise receiver/decryption. The PR description already states this distinction correctly, so this does not block the test-only commit.

Verification:

  • cargo test --locked: 258 passed, 8 ignored
  • Both new tests executed individually: 1 passed each
  • cargo clippy --locked: clean
  • Exact-head Rust, Flutter, and web checks: green
  • Clean merge-tree with live main (a149b8f160)
  • Working tree: clean

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