Skip to content

Extract shared welcome accept and catch-up helpers#495

Merged
justinmoon merged 4 commits intomasterfrom
welcome-helpers
Mar 8, 2026
Merged

Extract shared welcome accept and catch-up helpers#495
justinmoon merged 4 commits intomasterfrom
welcome-helpers

Conversation

@justinmoon
Copy link
Copy Markdown
Collaborator

@justinmoon justinmoon commented Mar 8, 2026

Summary

  • Extract shared key package interop helpers — moves key package logic into pika-marmot-runtime for reuse
  • Route remaining key package fetches through shared helper — completes key package interop migration
  • Characterize welcome lifecycle drift — adds tests documenting divergence between sidecar and core welcome handling
  • Extract shared welcome accept catch-up helper — pulls welcome accept + group catch-up logic into pika-marmot-runtime, reducing duplication between sidecar daemon and core

Net: ~590 lines added, ~50 removed — consolidating welcome/key-package handling into the shared runtime crate.

Test plan

  • CI passes (cargo check, cargo test, clippy)

🤖 Generated with Claude Code


Open with Devin

Summary by CodeRabbit

  • New Features

    • Added enhanced welcome acceptance workflow with optional message backfill functionality from relay sources.
    • Implemented automatic group message subscription and member tracking upon group acceptance.
  • Tests

    • Added comprehensive test suite verifying welcome lifecycle handling, eager acceptance behavior, and duplicate prevention during re-delivery.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 8, 2026

📝 Walkthrough

Walkthrough

The changes introduce a new welcome acceptance and catch-up flow in the pika-marmot-runtime, adding AcceptedWelcome struct and accept_welcome_and_catch_up function that supports optional message backfilling. The sidecar daemon integrates this new runtime helper, replacing direct welcome acceptance with subscription and backfill capabilities. Supporting test coverage is added.

Changes

Cohort / File(s) Summary
Core Runtime API
crates/pika-marmot-runtime/src/lib.rs
Extended IngestedWelcome with mls_group_id and ingested_messages fields. Introduced new AcceptedWelcome struct with wrapper_event_id, welcome_event_id, nostr_group_id_hex, mls_group_id, group_name, and ingested_messages. Added public accept_welcome_and_catch_up function with hook-based callback for post-acceptance operations and optional relay-based message backfill.
Sidecar Integration
crates/pikachat-sidecar/src/daemon.rs, crates/pikachat-sidecar/Cargo.toml
Replaced direct mdk.accept_welcome calls with new accept_welcome_with_backfill helper. Added ACCEPT_WELCOME_BACKLOG_LIMIT constant. Refactored daemon flow to subscribe to group messages after acceptance, queue ingested messages, and update downstream logic to use AcceptedWelcome fields for GroupJoined, WelcomeReceived emissions. Added tempfile dev-dependency for test support.
CLI Documentation
cli/src/main.rs
Added clarifying comment to cmd_accept_welcome indicating CLI accept remains narrow (joins locally without subscribe or backfill; other flows handle demand-based catchup).
Test Coverage
rust/src/core/mod.rs
Introduced welcome_lifecycle_characterization test module covering eager MLS welcome handling, gift-wrap processing, and group redelivery scenarios. Note: Test module content appears duplicated in the file. Added log comment documenting eager welcome handling behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Daemon as Pikachat Daemon
    participant Marmot as Pika-Marmot Runtime
    participant MDK
    participant Relay as Relay (Optional)
    participant Storage

    Client->>Daemon: Receive giftwrapped welcome event
    Daemon->>Marmot: accept_welcome_and_catch_up(mdk, client, relay_urls, welcome, after_accept)
    activate Marmot
    Marmot->>MDK: Extract welcome & create group
    MDK->>Storage: Store MLS group state
    Storage-->>MDK: ✓ Group created with mls_group_id
    MDK-->>Marmot: IngestedWelcome
    
    alt Relay URLs provided
        Marmot->>Relay: Query for backlog messages (limit: 2048)
        Relay-->>Marmot: Historical messages from relay
        Marmot->>Storage: Ingest backlog messages
        Storage-->>Marmot: ✓ Messages stored
    else No Relay URLs
        Note over Marmot: Skip backfill, proceed with hook
    end
    
    Marmot->>Marmot: Construct AcceptedWelcome with all fields
    deactivate Marmot
    Marmot->>Daemon: Return AcceptedWelcome(wrapper_event_id, welcome_event_id, nostr_group_id_hex, mls_group_id, group_name, ingested_messages)
    
    Daemon->>Daemon: after_accept hook executes
    Daemon->>Client: Subscribe to group messages (nostr_group_id_hex)
    Client-->>Daemon: ✓ Subscription active
    
    Daemon->>Daemon: Emit GroupJoined, WelcomeReceived events
    Daemon->>Daemon: Track and publish member counts
    Daemon-->>Client: Group accepted and synced
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A welcome accepted, with messages caught!
The marmot runtime tied knots,
Backfill and subscribe, group state in place,
The sidecar daemon keeps up the pace!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Extract shared welcome accept and catch-up helpers' directly and clearly summarizes the main change—moving shared welcome-handling logic into pika-marmot-runtime.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch welcome-helpers

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

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +206 to +219
mdk.accept_welcome(welcome).context("accept welcome")?;
after_accept(&accepted).await?;

if !relay_urls.is_empty() {
accepted.ingested_messages = ingest_group_backlog(
mdk,
client,
relay_urls,
&accepted.nostr_group_id_hex,
seen,
limit,
)
.await
.context("ingest accepted welcome backlog")?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Subscription leak and misleading error on backlog fetch failure after successful welcome accept

In accept_welcome_and_catch_up (crates/pika-marmot-runtime/src/lib.rs:206-207), mdk.accept_welcome() is called and succeeds before after_accept and ingest_group_backlog. If the backlog fetch at line 210-219 fails (e.g., network timeout), the ? propagates the error, causing the entire function to return Err. In the daemon caller (crates/pikachat-sidecar/src/daemon.rs:2618), this triggers the error branch which sends an mdk_error reply to the client — but the welcome was already irrevocably accepted in MLS state. This causes two problems: (1) the client is told the accept failed and may retry (which will then fail because the welcome is already consumed), and (2) the relay subscription created in the after_accept hook (stored in the subscribed_group mutex at line 2555) is never .take()-ed and inserted into group_subs (line 2574 is only reached on Ok), so the subscription leaks — it remains active on the relay but is untracked for cleanup.

Prompt for agents
In crates/pika-marmot-runtime/src/lib.rs, the accept_welcome_and_catch_up function at lines 206-219 should not propagate errors from ingest_group_backlog as hard failures since the welcome has already been irrevocably accepted at line 206. The backlog fetch error should be logged/warned but the function should still return Ok(accepted) with an empty ingested_messages list, or alternatively the AcceptedWelcome struct should include an optional backlog error field. Additionally, in crates/pikachat-sidecar/src/daemon.rs lines 2618-2620, the error branch should check the subscribed_group mutex and clean up (unsubscribe or add to group_subs) any subscription that was created in the after_accept hook before reporting the error.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

@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)
rust/src/core/mod.rs (1)

3907-3929: ⚠️ Potential issue | 🟠 Major

Route core welcome handling through the shared runtime helper.

This branch still does inline process_welcome/accept_welcome, so core does not pick up the shared accept+catch-up flow this PR introduced. That leaves core/sidecar lifecycle drift in place and can still miss pre-subscription backlog for newly joined groups.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/core/mod.rs` around lines 3907 - 3929, Core currently calls
sess.mdk.accept_welcome(&welcome) inline, bypassing the shared runtime helper
that runs accept+catch-up; replace this inline accept_welcome usage with a call
into the shared "process_welcome" runtime helper (the new accept+catch-up
wrapper), passing the same session/context and welcome so core uses the shared
accept+catch-up flow; preserve the existing error handling (tracing::error and
self.toast) and keep the subsequent key-package rotation
(referenced_key_package_event_id, delete_event_best_effort,
ensure_key_package_published_best_effort) and refresh_all_from_storage calls.
🧹 Nitpick comments (2)
rust/src/core/mod.rs (1)

8048-8145: Don’t lock this test to eager acceptance timing.

The new test asserts that a welcome must leave no pending entry immediately on receipt, which hard-codes today’s core-specific eager flow. If core is migrated to the shared helper next, this characterization test will fail even if the real invariant still holds. Prefer asserting “no duplicate active group / no ratchet reset” without depending on immediate pending-state emptiness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/core/mod.rs` around lines 8048 - 8145, Test
gift_wrap_received_accepts_immediately_and_skips_duplicate_pending_state is
locked to an “eager accept” timing by asserting get_pending_welcomes(...) is
empty immediately after the first handle_internal call; remove that strict
assertion and instead only assert the invariant that no duplicate active group
is created and no ratchet/reset occurs on re-delivery. Concretely: delete the
assert block that checks get_pending_welcomes(...).is_empty() right after the
first InternalEvent::GiftWrapReceived, keep the check that groups.len() == 1
after first delivery, and retain the subsequent re-delivery assertions
(groups.len() still == 1 and no duplicate active group). If you can access a
ratchet/state identifier on the group (via the mdk API), add an assertion that
its identifier/state did not change across deliveries; otherwise do not assert
immediate pending-list emptiness.
crates/pika-marmot-runtime/src/lib.rs (1)

206-219: Partial completion on backlog failure may complicate retry logic.

If after_accept or ingest_group_backlog fails, the welcome has already been accepted (line 206), but the function returns an error and the caller does not receive AcceptedWelcome. A subsequent retry by the caller would fail since the welcome is no longer pending in MDK.

If this is intentional (callers should handle the error and query for the accepted group separately), consider documenting it in the docstring. Otherwise, consider returning AcceptedWelcome even when backfill fails so the caller knows the acceptance succeeded:

💡 Alternative: non-fatal backfill approach
     if !relay_urls.is_empty() {
-        accepted.ingested_messages = ingest_group_backlog(
+        // Backfill failure shouldn't prevent returning the accepted welcome.
+        if let Ok(msgs) = ingest_group_backlog(
             mdk,
             client,
             relay_urls,
             &accepted.nostr_group_id_hex,
             seen,
             limit,
         )
-        .await
-        .context("ingest accepted welcome backlog")?;
+        .await
+        {
+            accepted.ingested_messages = msgs;
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/pika-marmot-runtime/src/lib.rs` around lines 206 - 219, The current
flow accepts the welcome via mdk.accept_welcome(welcome) then calls
after_accept(&accepted).await and ingest_group_backlog(...).await, but if those
post-accept steps fail the function returns an error and the caller never
receives the AcceptedWelcome; change the function so acceptance is final and the
AcceptedWelcome is returned even when after_accept or ingest_group_backlog fail:
catch errors from after_accept and ingest_group_backlog (wrap their .await calls
in match or .await.map_err handling), log or attach the error context to the
returned AcceptedWelcome (e.g., set a field on accepted or return a tuple/struct
containing AcceptedWelcome plus backfill error info) instead of propagating the
error, ensuring mdk.accept_welcome(welcome) success always results in returning
AcceptedWelcome to callers so retries don’t re-accept the same welcome.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@rust/src/core/mod.rs`:
- Around line 3907-3929: Core currently calls sess.mdk.accept_welcome(&welcome)
inline, bypassing the shared runtime helper that runs accept+catch-up; replace
this inline accept_welcome usage with a call into the shared "process_welcome"
runtime helper (the new accept+catch-up wrapper), passing the same
session/context and welcome so core uses the shared accept+catch-up flow;
preserve the existing error handling (tracing::error and self.toast) and keep
the subsequent key-package rotation (referenced_key_package_event_id,
delete_event_best_effort, ensure_key_package_published_best_effort) and
refresh_all_from_storage calls.

---

Nitpick comments:
In `@crates/pika-marmot-runtime/src/lib.rs`:
- Around line 206-219: The current flow accepts the welcome via
mdk.accept_welcome(welcome) then calls after_accept(&accepted).await and
ingest_group_backlog(...).await, but if those post-accept steps fail the
function returns an error and the caller never receives the AcceptedWelcome;
change the function so acceptance is final and the AcceptedWelcome is returned
even when after_accept or ingest_group_backlog fail: catch errors from
after_accept and ingest_group_backlog (wrap their .await calls in match or
.await.map_err handling), log or attach the error context to the returned
AcceptedWelcome (e.g., set a field on accepted or return a tuple/struct
containing AcceptedWelcome plus backfill error info) instead of propagating the
error, ensuring mdk.accept_welcome(welcome) success always results in returning
AcceptedWelcome to callers so retries don’t re-accept the same welcome.

In `@rust/src/core/mod.rs`:
- Around line 8048-8145: Test
gift_wrap_received_accepts_immediately_and_skips_duplicate_pending_state is
locked to an “eager accept” timing by asserting get_pending_welcomes(...) is
empty immediately after the first handle_internal call; remove that strict
assertion and instead only assert the invariant that no duplicate active group
is created and no ratchet/reset occurs on re-delivery. Concretely: delete the
assert block that checks get_pending_welcomes(...).is_empty() right after the
first InternalEvent::GiftWrapReceived, keep the check that groups.len() == 1
after first delivery, and retain the subsequent re-delivery assertions
(groups.len() still == 1 and no duplicate active group). If you can access a
ratchet/state identifier on the group (via the mdk API), add an assertion that
its identifier/state did not change across deliveries; otherwise do not assert
immediate pending-list emptiness.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 09611c12-f6a0-4218-b88b-3f7ea3c5c8c8

📥 Commits

Reviewing files that changed from the base of the PR and between 9f4258b and 1d66a41.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • cli/src/main.rs
  • crates/pika-marmot-runtime/src/lib.rs
  • crates/pikachat-sidecar/Cargo.toml
  • crates/pikachat-sidecar/src/daemon.rs
  • rust/src/core/mod.rs

@justinmoon justinmoon merged commit 6b59aee into master Mar 8, 2026
18 checks passed
@justinmoon justinmoon deleted the welcome-helpers branch March 20, 2026 14:52
@justinmoon justinmoon restored the welcome-helpers branch March 20, 2026 21:20
@justinmoon justinmoon deleted the welcome-helpers branch March 20, 2026 21:21
@justinmoon justinmoon restored the welcome-helpers branch March 20, 2026 21:49
@justinmoon justinmoon deleted the welcome-helpers branch March 20, 2026 21:53
@justinmoon justinmoon restored the welcome-helpers branch March 20, 2026 21:58
@justinmoon justinmoon deleted the welcome-helpers branch March 20, 2026 21:59
@justinmoon justinmoon restored the welcome-helpers branch March 20, 2026 22:04
@justinmoon justinmoon deleted the welcome-helpers branch March 20, 2026 22:04
@justinmoon justinmoon restored the welcome-helpers branch March 20, 2026 22:10
@justinmoon justinmoon deleted the welcome-helpers branch March 20, 2026 22:10
@justinmoon justinmoon restored the welcome-helpers branch March 20, 2026 22:15
@justinmoon justinmoon deleted the welcome-helpers branch March 20, 2026 22:20
@justinmoon justinmoon restored the welcome-helpers branch March 20, 2026 22:21
@justinmoon justinmoon deleted the welcome-helpers branch March 20, 2026 22:26
@justinmoon justinmoon deleted the welcome-helpers branch March 21, 2026 18:31
@justinmoon justinmoon restored the welcome-helpers branch March 21, 2026 18:36
@justinmoon justinmoon deleted the welcome-helpers branch March 21, 2026 18:36
@justinmoon justinmoon restored the welcome-helpers branch March 21, 2026 18:42
@justinmoon justinmoon deleted the welcome-helpers branch March 21, 2026 18:46
@justinmoon justinmoon restored the welcome-helpers branch March 21, 2026 18:47
@justinmoon justinmoon deleted the welcome-helpers branch March 21, 2026 18:52
@justinmoon justinmoon restored the welcome-helpers branch March 21, 2026 18:53
@justinmoon justinmoon deleted the welcome-helpers branch March 21, 2026 18:57
@justinmoon justinmoon restored the welcome-helpers branch March 21, 2026 18:59
@justinmoon justinmoon deleted the welcome-helpers branch March 21, 2026 19:02
@justinmoon justinmoon restored the welcome-helpers branch March 21, 2026 19:04
@justinmoon justinmoon deleted the welcome-helpers branch March 21, 2026 19:08
@justinmoon justinmoon restored the welcome-helpers branch March 21, 2026 19:10
@justinmoon justinmoon deleted the welcome-helpers branch March 21, 2026 19:13
@justinmoon justinmoon restored the welcome-helpers branch March 21, 2026 19:16
@justinmoon justinmoon deleted the welcome-helpers branch March 21, 2026 19:18
@justinmoon justinmoon restored the welcome-helpers branch March 21, 2026 19:21
@justinmoon justinmoon deleted the welcome-helpers branch March 21, 2026 19:24
@justinmoon justinmoon restored the welcome-helpers branch March 21, 2026 19:27
@justinmoon justinmoon deleted the welcome-helpers branch March 21, 2026 19:29
@justinmoon justinmoon restored the welcome-helpers branch March 21, 2026 19:33
@justinmoon justinmoon deleted the welcome-helpers branch March 21, 2026 19:34
@justinmoon justinmoon restored the welcome-helpers branch March 21, 2026 19:39
@justinmoon justinmoon deleted the welcome-helpers branch March 21, 2026 19:40
@justinmoon justinmoon restored the welcome-helpers branch March 21, 2026 19:44
@justinmoon justinmoon deleted the welcome-helpers branch March 21, 2026 19:45
@justinmoon justinmoon restored the welcome-helpers branch March 21, 2026 19:50
@justinmoon justinmoon deleted the welcome-helpers branch March 21, 2026 19:55
@justinmoon justinmoon restored the welcome-helpers branch March 21, 2026 19:59
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