Skip to content

perf(events): share wa::Message via Arc end-to-end (zero deep-clone on dispatch) - #613

Merged
jlucaso1 merged 6 commits into
oxidezap:mainfrom
Salientekill:feat/message-context-arc
May 5, 2026
Merged

perf(events): share wa::Message via Arc end-to-end (zero deep-clone on dispatch)#613
jlucaso1 merged 6 commits into
oxidezap:mainfrom
Salientekill:feat/message-context-arc

Conversation

@Salientekill

@Salientekill Salientekill commented May 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Make wa::Message shareable via Arc from the dispatch site all the way to user
handlers, eliminating the deep-clone that previously happened on every received
message.

Motivation

wa::Message carries media buffers and extended-text payloads (often >1 KB).
On the inbound path it was deep-cloned twice:

  1. Client::dispatch_messageEvent::Message(Box::new(msg), ...) (owned move into Box, fine)
  2. MessageContext::from_eventfrom_parts(&msg, ...)Box::new(message.clone()) ← deep clone

Downstream bots that re-spawned per message (tokio::spawn with a cloned context
for fire-and-forget side effects) cloned the whole message again on every spawn.

Changes

Core (wacore)

  • Event::Message(Box<wa::Message>, Arc<MessageInfo>)Event::Message(Arc<wa::Message>, Arc<MessageInfo>)
  • Event::as_message() now returns Option<(&Arc<wa::Message>, &MessageInfo)> so
    callers that need to share ownership can Arc::clone cheaply

Client (whatsapp-rust)

  • MessageContext::message: Box<wa::Message>Arc<wa::Message>
  • MessageContext derives Clone (refcount bump only)
  • MessageContext::from_event clones the Arc instead of deep-cloning the message
  • New MessageContext::from_arc(Arc<wa::Message>, ...) constructor for callers
    that already own an Arc
  • Dispatch sites in src/message.rs and src/pdo.rs switched from
    Box::new(...) to Arc::new(...)

Compatibility (pre-1.0 breakage)

  • Event::Message variant signature changed (BoxArc). Pattern matches
    like Event::Message(msg, info) keep working since both Box and Arc
    deref to &wa::Message. Code that explicitly constructed
    Event::Message(Box::new(m), info) needs to switch to Arc::new.
  • Event::as_message() return type changed; field access via the returned
    &Arc<wa::Message> keeps compiling thanks to deref coercion.
  • MessageContext.message type changed but the field stays pub. Reading via
    ctx.message.field continues to work via Arc deref. Code that moved out
    of the field (let m = *ctx.message) needs (*ctx.message).clone().

Precedent

The pattern matches what other Rust messaging libs do for shareable per-event
context: serenity's Context (Arc-internal, Clone-cheap), matrix-rust-sdk's
Room/Client, teloxide's Bot. Message payload itself is owned/Arc-shared
rather than boxed.

Test plan

  • cargo fmt --all
  • cargo clippy --workspace --exclude e2e-tests --tests — no new warnings
  • cargo test --workspace --exclude e2e-tests --lib — 1378 passed, 0 failed
  • cargo check covers e2e-tests compilation (full e2e run requires the mock server)

Cloning MessageContext (e.g., re-spawning into a fire-and-forget background
task) deep-copied wa::Message every time. wa::Message carries media buffers
and extended_text payloads (often >1 KB) — significant overhead on hot paths
that fan out per-message work (emoji-challenge dispatch, sticker triggers).

Switching to Arc<wa::Message> makes Clone an O(1) refcount bump. New
from_arc() constructor lets callers that already own an Arc avoid the one
remaining clone in from_parts().

BREAKING (minor): the public field type changed from Box<wa::Message> to
Arc<wa::Message>. Read-only access (&self.message, .field, deref) is
unaffected. Code that moves out of the field (let msg = *ctx.message)
needs (*ctx.message).clone() instead.
@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown

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
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

MessageContext now stores the WhatsApp payload as a private Arc<wa::Message> (was pub Box<wa::Message>), derives Clone, adds from_arc, message(), message_arc(), and into_box() (which tries Arc::try_unwrap then clones); from_parts uses Arc::new(...). Tests added for allocation and clone semantics.

Changes

MessageContext Arc Refactor

Layer / File(s) Summary
Data Shape
src/bot.rs
MessageContext::message changed from pub Box<wa::Message> to private Arc<wa::Message>; MessageContext now #[derive(Clone)].
Core Implementation
src/bot.rs
Added private helper arc_into_box<T: Clone>(arc: Arc<T>) -> Box<T> using Arc::try_unwrap with clone fallback; from_parts now stores Arc::new(message.clone()); new from_arc(message: Arc<wa::Message>, ...) constructor; message(&self) -> &wa::Message and message_arc(&self) -> Arc<wa::Message> accessors; into_box(self) -> Box<wa::Message> delegates to arc_into_box.
Compatibility / Visibility
src/bot.rs
Previously-public message field is now private; callers must use message()/message_arc() or from_arc.
Tests
src/bot.rs (tests)
Adds tests: from_arc_preserves_allocation, into_box_with_unique_arc_consumes_strong_ref, into_box_with_shared_arc_preserves_other_strong_refs; path-level probe tests using CloneCounter to assert clone counts and uniqueness behavior.
Manifest
Cargo.toml
Updated (no dependency changes indicated in diffs).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main performance optimization: converting MessageContext.message from Box to Arc and adding the from_arc constructor.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the motivation, implementation details, breaking changes, and test coverage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bot.rs`:
- Around line 33-38: This change makes MessageContext.message a breaking API by
switching its type from Box<wa::Message> to Arc<wa::Message>; either revert the
field back to pub message: Box<wa::Message> to preserve ABI, or if you must keep
Arc, treat it as a breaking change: make the field private, bump the crate major
version, and add compatibility accessors/ctors on MessageContext such as pub fn
message_arc(&self) -> Arc<wa::Message> and pub fn into_box(self) ->
Box<wa::Message> (implementing into_box by unwrapping the Arc when unique or
cloning the inner wa::Message into a Box) plus From/Into impls to ease
migration; reference the MessageContext struct and its message field in your
changes.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5aef256d-c475-4cf4-9412-4002e29538b1

📥 Commits

Reviewing files that changed from the base of the PR and between 240d631 and 2e980f5.

📒 Files selected for processing (1)
  • src/bot.rs

Comment thread src/bot.rs Outdated
…ack)

Addresses CodeRabbit's review on PR oxidezap#613: making the public field type
change Box<wa::Message> → Arc<wa::Message> a controlled breaking change
instead of a silent ABI break.

Changes:
- 'message' field is now private; readers go through new accessors
- pub fn message(&self) -> &wa::Message — replaces direct field access
- pub fn message_arc(&self) -> Arc<wa::Message> — cheap Arc::clone for
  re-spawn / shared ownership without exposing the inner storage
- pub fn into_box(self) -> Box<wa::Message> — compatibility helper for
  legacy code; uses Arc::try_unwrap when the Arc is uniquely held,
  falling back to deep clone otherwise

Read-only access pattern stays nearly identical (ctx.message.field →
ctx.message().field). Re-spawns get a typed API for cheap sharing.
Old code that needed Box can call into_box() during migration.

Storage representation is now an implementation detail behind getters.
@Salientekill

Copy link
Copy Markdown
Contributor Author

Thanks for the review @coderabbitai — the breaking-change-via-public-field concern is fair. Pushed 1230131 which goes with your option (b):

  • message field is now private
  • new accessors:
    • pub fn message(&self) -> &wa::Message — replaces direct field access (ctx.message.fooctx.message().foo)
    • pub fn message_arc(&self) -> Arc<wa::Message> — cheap Arc::clone for re-spawn / shared ownership
    • pub fn into_box(self) -> Box<wa::Message> — compat helper using Arc::try_unwrap (zero-clone when uniquely held) with deep-clone fallback

Storage rep is now an implementation detail. Verified locally with cargo build --all + cargo test --lib + cargo clippy --all --tests, all green.

I left the version bump to the maintainer's discretion since it's part of release tagging upstream — happy to add it to the PR if you prefer.

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the current state of bot.rs for the MessageContext implementation
fd "bot.rs" --exec cat {}

Length of output: 40057


@Salientekill Good execution on this — the implementation is solid. Here's my assessment of commit 1230131:

What's working correctly:

  • message is properly private — storage representation is now a true implementation detail.
  • message() and message_arc() are correctly #[inline]'d and follow idiomatic Rust accessor patterns.
  • into_box() with Arc::try_unwrap + (*arc).clone() fallback is the correct pattern — zero-copy when uniquely held, graceful degradation otherwise.
  • build_quote_context still compiles cleanly — &self.message coerces from &Arc<wa::Message>&wa::Message via Deref, no manual &* needed.
  • ✅ Doc comments on the struct and each accessor are clear and explain the tradeoffs well.

One gap worth addressing — unit tests for MessageContext:

The test module currently only exercises BotBuilder. Given this is a public API change, I'd recommend adding targeted tests:

#[cfg(test)]
mod message_context_tests {
    use super::*;
    use std::sync::Arc;

    fn make_ctx() -> MessageContext {
        let msg = Arc::new(wa::Message::default());
        let info = MessageInfo::default(); // adjust if non-Default
        let client = Arc::new(/* minimal Client stub or use existing helper */);
        MessageContext::from_arc(msg, &info, client)
    }

    #[test]
    fn message_returns_ref() {
        let ctx = make_ctx();
        // just ensure it compiles and returns the expected type
        let _: &wa::Message = ctx.message();
    }

    #[test]
    fn message_arc_cheap_clone() {
        let ctx = make_ctx();
        let arc1 = ctx.message_arc();
        let arc2 = ctx.message_arc();
        assert!(Arc::ptr_eq(&arc1, &arc2)); // same allocation
    }

    #[test]
    fn into_box_unique_arc_zero_clone() {
        let msg = Arc::new(wa::Message::default());
        let ptr = Arc::as_ptr(&msg);
        let info = MessageInfo::default();
        let client = Arc::new(/* ... */);
        let ctx = MessageContext::from_arc(msg, &info, client);
        let boxed = ctx.into_box();
        // Uniquely held — pointer in Box should equal original Arc pointer
        assert_eq!(&*boxed as *const _, ptr);
    }

    #[test]
    fn into_box_shared_arc_deep_clones() {
        let msg = Arc::new(wa::Message::default());
        let _keeper = Arc::clone(&msg); // prevent try_unwrap from succeeding
        let info = MessageInfo::default();
        let client = Arc::new(/* ... */);
        let ctx = MessageContext::from_arc(msg, &info, client);
        let _boxed = ctx.into_box(); // must not panic; verifies fallback path
    }
}

The Arc::ptr_eq assertion on the unique-ownership path is the key correctness check for the zero-copy guarantee you're advertising.

On the version bump — agreed, that's the right call to leave it to the maintainer.

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bot.rs`:
- Around line 58-91: Add unit tests that exercise the Arc compatibility
boundary: create a wa::Message, wrap it in Arc and construct the context with
from_arc(), assert message() returns expected data and message_arc() yields an
Arc that points to the same message (Arc::ptr_eq). Then test into_box() in two
scenarios: (1) when the Arc is uniquely held (drop any clones before calling
into_box()) assert the returned Box contains the original message instance
(i.e., no cloning by verifying identity via ptr or by checking a side-effect
field), and (2) when the Arc is shared (keep another Arc clone alive) call
into_box() and assert a cloned Box is returned (distinct identity but equal
content). Use the functions from_arc, message_arc, message, and into_box to
locate code under test.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fdad191a-723a-43e7-9d58-85beccc9f182

📥 Commits

Reviewing files that changed from the base of the PR and between 2e980f5 and 1230131.

📒 Files selected for processing (1)
  • src/bot.rs

Comment thread src/bot.rs Outdated
Endereça segundo review do CodeRabbit no PR oxidezap#613 — pediu testes
focados nos métodos novos.

3 testes novos:
- from_arc_preserves_allocation: from_arc não deep-clona; message_arc()
  retorna o mesmo Arc allocation (Arc::ptr_eq via Arc::as_ptr).
- into_box_reclaims_when_unique: refcount==1 → try_unwrap path consome
  o Arc; weak sentinel fica órfão (strong_count=0, upgrade=None).
- into_box_clones_when_shared: refcount>1 → fallback clone preserva
  conteúdo + kept Arc retorna a strong_count=1, alocação distinta da
  do Box (verificado via std::ptr::eq negativo).

Pointer identity comentada onde não funciona (Box::new realloca o T
mesmo após Arc::try_unwrap por mover o T pra novo heap slot).
@Salientekill

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai — added the unit tests in af03118:

  • from_arc_preserves_allocation — verifies message_arc() returns the same allocation as the input Arc (std::ptr::eq on Arc::as_ptr).
  • into_box_reclaims_when_unique — when refcount is 1, observes the try_unwrap path via a Weak sentinel (after into_box, weak.strong_count() == 0 and upgrade() is None).
  • into_box_clones_when_shared — keeps a second Arc outstanding so try_unwrap falls through to deep-clone; verifies content preserved + kept Arc back to strong_count == 1 + boxed allocation distinct from the kept Arc payload.

One subtlety I documented inline: pointer identity for Arc::try_unwrapBox::new is not observable, because Box::new always allocates fresh heap and moves the T there. So the unique-case test uses a Weak orphan check rather than ptr::eq (my first attempt at pointer identity rightfully failed).

Full suite green: 452 + 22 + 0 + 2 passed.

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@Salientekill — yeah, this is working the right way. The three tests are clean and complete:

  • from_arc_preserves_allocationArc::as_ptr + std::ptr::eq is exactly the right signal; it directly verifies the zero-copy guarantee at the Arc level.
  • into_box_reclaims_when_unique — the Weak orphan check is the correct approach here. You caught a real flaw in my suggested test. Arc::try_unwrap moves the T out of the Arc allocation; Box::new then places it at a new address, so pointer identity was never a valid assertion. The Weak going to strong_count == 0 is what actually proves the try_unwrap path fired.
  • into_box_clones_when_shared — distinct boxed allocation + strong_count == 1 on the kept Arc + content preserved covers all three properties of the fallback path. That's a tight set of assertions.

452 + 22 + 0 + 2 all green. The implementation and the test suite are both solid. This PR is in good shape.


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bot.rs`:
- Around line 1163-1179: The test `into_box_reclaims_when_unique` currently only
proves the last `Arc` was dropped and doesn't guarantee `Arc::try_unwrap` was
used; either add an explicit probe that can distinguish the try_unwrap path
(e.g., wrap the message payload in a small type with a Drop side-effect or a
flag that can detect whether the original payload was moved vs cloned and assert
that move-path occurred when calling `MessageContext::into_box` on an `Arc`
created via `MessageContext::from_arc`), or change the test name/comment to no
longer claim coverage of `Arc::try_unwrap`/zero-clone behavior; reference
`into_box`, `MessageContext::from_arc`, `into_box_reclaims_when_unique`, and
`Arc::try_unwrap` when locating the fix.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5d87fa58-3101-4580-9032-1ed6f68197e4

📥 Commits

Reviewing files that changed from the base of the PR and between 1230131 and af03118.

📒 Files selected for processing (1)
  • src/bot.rs

Comment thread src/bot.rs Outdated
CodeRabbit aponta corretamente que os testes anteriores
(`into_box_reclaims_when_unique` / `_clones_when_shared`) só validavam
o estado pós-into_box (Arc consumido / kept survives) — uma regressão
hipotética que sempre clonasse passaria igual.

Mudanças:
- Renomeados os state-level tests pra refletir o que provam
  (`into_box_with_unique_arc_consumes_strong_ref` /
  `into_box_with_shared_arc_preserves_other_strong_refs`).
- Comentários inline explicitam que são state-level e apontam pro
  path-level probe.
- Novo módulo de teste algorítmico:
  - `CloneCounter`: payload com Drop side-effect via AtomicUsize que
    registra cada Clone::clone.
  - `try_unwrap_or_clone<T>()`: réplica genérica do pattern de
    `into_box` (a fn de produção é monomorfizada em wa::Message).
  - `into_box_skips_clone_when_unique`: refcount==1 → counter == 0.
  - `into_box_invokes_clone_exactly_once_when_shared`: refcount>1 →
    counter == 1.

Cobertura ironclad da fn de produção exigiria torná-la genérica;
documentado inline como out-of-scope. Os tests algorítmicos garantem
que o pattern descrito no PR se comporta como anunciado.
@Salientekill

Copy link
Copy Markdown
Contributor Author

Good catch @coderabbitai — you're right, the state-level tests can't distinguish try_unwrap → Ok from "always clone, then drop". Pushed 8fb06bb with both fixes:

Renamed the existing tests to reflect what they actually verify (state, not path):

  • into_box_reclaims_when_uniqueinto_box_with_unique_arc_consumes_strong_ref
  • into_box_clones_when_sharedinto_box_with_shared_arc_preserves_other_strong_refs

Inline comments now explicitly say these are state-level and point to the path-level probe.

Added a path-level probe in a separate test block:

  • CloneCounter: a payload type whose Clone impl bumps a shared AtomicUsize so cloning is observable.
  • try_unwrap_or_clone<T>: a free-function mirror of into_box's body. The production version is monomorphized over wa::Message, so I kept this generic helper alongside it for testability.
  • into_box_skips_clone_when_unique — refcount 1 → counter stays at 0.
  • into_box_invokes_clone_exactly_once_when_shared — refcount 2 → counter == 1.

The honest caveat (called out inline): a regression that swaps MessageContext::into_box's body wholesale would slip past this — the algorithmic test only locks down the pattern, not its concrete monomorphization. Truly ironclad coverage would require making into_box itself generic over the payload, which I noted as out-of-scope.

Test suite: 454 + 22 + 0 + 2 passed; cargo clippy --all --tests clean.

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@Salientekill — this is exactly right, and the approach is correct.

CloneCounter + AtomicUsize is the standard Rust pattern for making side effects of Clone observable in tests. Using it here turns what was previously an unprovable assumption ("the try_unwrap path fired") into a hard assertion.

try_unwrap_or_clone<T> as a generic free function is the right tradeoff for testability. The alternative — making MessageContext::into_box itself generic over its payload — would change the production API for the sole benefit of test coverage, which is the wrong tradeoff at this stage.

The caveat you documented inline is honest and correct: a regression that replaced the try_unwrap body wholesale would evade the generic probe. But that's the inherent limit of white-box algorithmic testing versus black-box monomorphization testing, and the comment makes that boundary explicit so a future reader understands what the test does and doesn't guarantee.

What the suite now covers end-to-end:

Test Verifies
from_arc_preserves_allocation Zero-copy at Arc level
into_box_with_unique_arc_consumes_strong_ref State: strong count goes to 0 on unique path
into_box_with_shared_arc_preserves_other_strong_refs State: other strong refs unaffected on shared path
into_box_skips_clone_when_unique Path: Clone not invoked when refcount == 1
into_box_invokes_clone_exactly_once_when_shared Path: Clone invoked exactly once when refcount == 2

That's a complete picture. 454 + 22 all green and clippy clean — the implementation and test suite are both in good shape. This PR is ready.

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/bot.rs`:
- Around line 1183-1186: The comment in src/bot.rs incorrectly references the
test name; update the shared-path comment to cite
into_box_invokes_clone_exactly_once_when_shared (the test that asserts the
"clone exactly once" guarantee) instead of into_box_skips_clone_when_unique, so
the comment matches the actual test coverage and avoids confusion when
inspecting functions like into_box and its test helpers.
- Around line 1232-1239: The function try_unwrap_or_clone duplicates
MessageContext::into_box's Arc::try_unwrap/clone logic; extract that match into
a single private generic helper (e.g., fn into_box_from_arc<T: Clone>(arc:
Arc<T>) -> Box<T>) and replace both try_unwrap_or_clone and
MessageContext::into_box to call this helper so there is one canonical
implementation used by production and tests; ensure the helper is generic over
T: Clone and returns Box<T>, and update both callers to delegate to it.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f04be700-b0e5-4622-b3e5-cde7cd8bb75e

📥 Commits

Reviewing files that changed from the base of the PR and between af03118 and 8fb06bb.

📒 Files selected for processing (1)
  • src/bot.rs

Comment thread src/bot.rs Outdated
Comment thread src/bot.rs Outdated
Endereça duas observações do CodeRabbit no PR oxidezap#613:

1. (Major nitpick) Eliminar duplicação prod/test do algoritmo. Antes
   existia tanto MessageContext::into_box quanto try_unwrap_or_clone
   (em tests) com a mesma match Arc::try_unwrap. Helper privado
   genérico arc_into_box<T: Clone>(arc) -> Box<T> agora é a única
   fonte da verdade — into_box delega, e os path-level tests chamam
   o mesmo helper. Regressão na produção sai imediatamente nos tests.

2. (Minor) Stale ref no comentário do state-test 'shared'. Apontava
   pra into_box_skips_clone_when_unique; corrigido pra
   into_box_invokes_clone_exactly_once_when_shared (que é onde a
   garantia 'clone exactly once' é asserida).
@Salientekill

Copy link
Copy Markdown
Contributor Author

Both fixed in 8c119ed — and you nailed it on the duplication call, that closes the gap I'd written off as out-of-scope:

Single source of truth. New private helper at module scope:

fn arc_into_box<T: Clone>(arc: Arc<T>) -> Box<T> {
    match Arc::try_unwrap(arc) {
        Ok(value) => Box::new(value),
        Err(arc) => Box::new((*arc).clone()),
    }
}

MessageContext::into_box is now a one-liner that delegates to it. The path-level tests (into_box_skips_clone_when_unique / into_box_invokes_clone_exactly_once_when_shared) call arc_into_box directly — exactly the same code path production exercises. A future regression that swaps into_box's body would now have to rewrite the helper too, and the algorithmic tests would catch it.

Stale ref fix. Updated the comment on into_box_with_shared_arc_preserves_other_strong_refs to point to into_box_invokes_clone_exactly_once_when_shared (where the 'clone exactly once' guarantee actually lives).

cargo test --lib: 454 + 22 + 0 + 2 passed. cargo clippy --all --tests clean. cargo fmt clean.

Move `Event::Message` from `Box<wa::Message>` to `Arc<wa::Message>` and
route `MessageContext::from_event` through `Arc::clone`, eliminating the
deep-clone that previously happened on every dispatched message.

Also trims the `MessageContext` API back to what's actually used in-tree:

- field stays `pub` (no accessor methods); `Arc::clone(&ctx.message)`
  is the natural way to share, matching serenity / matrix-sdk
- drops `into_box` and `arc_into_box` helper (no callers)
- single test verifying `from_arc` doesn't deep-clone (Arc::as_ptr equality)

Updated dispatch sites: `src/message.rs`, `src/pdo.rs` now `Arc::new`
instead of `Box::new`. `Event::as_message` returns `&Arc<wa::Message>`
so callers that need the Arc can clone cheaply; auto-deref keeps
existing `msg.field` access compiling.
@jlucaso1 jlucaso1 changed the title perf(bot): MessageContext.message Box→Arc + from_arc constructor perf(events): share wa::Message via Arc end-to-end (zero deep-clone on dispatch) May 5, 2026

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/bot.rs (1)

36-41: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

message is still pub — the accessor methods described in the PR are absent.

The past review flagged this and was marked ✅ resolved (commit 1230131), and the PR description explicitly lists message as made private with accessors message(), message_arc(), and into_box(). But the code tells a different story — the field is still pub message: Arc<wa::Message> and none of those accessor methods appear anywhere in the impl MessageContext block (lines 43–117).

This needs to be correct. Either:

  • The accessors were never committed (implementation gap), or
  • The field was intended to stay public (but then the PR description is wrong and the past review resolution was premature).

If the field stays pub, at minimum the PR description and the "addressed" resolution on the past review need to be corrected. If the field should be private, those methods need to actually land.

🤖 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 `@src/bot.rs` around lines 36 - 41, The MessageContext struct still exposes pub
message: Arc<wa::Message> despite the PR and past review claiming it was made
private with accessors; either remove pub from the message field and implement
the missing accessors message(), message_arc(), and into_box() on impl
MessageContext, or if you intentionally want the field public, update the PR
description and prior review resolution to reflect that decision. Specifically,
make the field private (remove pub from message) and add these methods on
MessageContext: message() to return an immutable reference to the inner
wa::Message, message_arc() to return a cloned Arc<wa::Message>, and into_box()
to consume the context (or its Arc) and return an owned/boxed wa::Message as
intended; ensure the method names match exactly so reviewers can verify the
change.
🤖 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 `@src/bot.rs`:
- Around line 1099-1104: The test directly accesses the public field
ctx.message, which couples the test to the field being pub; change the assertion
to use the public accessor (e.g., MessageContext::message_arc() or a
message_arc() method) so it compares Arc::as_ptr(&ctx.message_arc()) to
original_ptr instead of Arc::as_ptr(&ctx.message); update the test to call
MessageContext::from_arc(...) as before and replace any direct field access with
the accessor to ensure the test remains valid if MessageContext makes the field
private.

---

Duplicate comments:
In `@src/bot.rs`:
- Around line 36-41: The MessageContext struct still exposes pub message:
Arc<wa::Message> despite the PR and past review claiming it was made private
with accessors; either remove pub from the message field and implement the
missing accessors message(), message_arc(), and into_box() on impl
MessageContext, or if you intentionally want the field public, update the PR
description and prior review resolution to reflect that decision. Specifically,
make the field private (remove pub from message) and add these methods on
MessageContext: message() to return an immutable reference to the inner
wa::Message, message_arc() to return a cloned Arc<wa::Message>, and into_box()
to consume the context (or its Arc) and return an owned/boxed wa::Message as
intended; ensure the method names match exactly so reviewers can verify the
change.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: df6e7ca8-02fc-4659-9376-cd1bd48bb00a

📥 Commits

Reviewing files that changed from the base of the PR and between 8fb06bb and 5826fe6.

📒 Files selected for processing (4)
  • src/bot.rs
  • src/message.rs
  • src/pdo.rs
  • wacore/src/types/events.rs

Comment thread src/bot.rs
@jlucaso1
jlucaso1 merged commit ceb62b8 into oxidezap:main May 5, 2026
16 of 17 checks passed
alexandme added a commit to Micra-io/zeroclaw that referenced this pull request May 6, 2026
…estore Apr-24 protocol parity (#52)

* chore(deps)!: pin wa-rs* to Micra-io/whatsapp-rust rebrand fork

Bump wa-rs* version constraints from "0.2" → "0.5" and add
[patch.crates-io] redirecting all 10 wa-rs* crates at the
wa-rs-rebrand-2026-05 branch of the Micra-io/whatsapp-rust fork
of oxidezap/whatsapp-rust.

Build is intentionally broken at this commit — Task 4 patches
src/channels/whatsapp_web.rs for the breaking deltas surfaced by
the new wa-rs (109 compile errors expected: Arc<Event> wrapping
from oxidezap/whatsapp-rust#613, BotBuilder typestate change from
zeroclaw-labs#586 DevicePropsOverride, removed methods, etc.).

Refs:
- docs/plans/2026-05-06-001-wa-rs-rebrand-plan.md
- #50
- zeroclaw-labs#6246

* fix(channels/whatsapp): adapt to wa-rs 0.5 breaking changes

The 0.2 → 0.5 wa-rs bump (Micra-io rebrand of oxidezap/whatsapp-rust)
exposed 109 cascading API errors beyond the originally-scoped
with_device_props change. This commit rebuilds ZeroClaw's WhatsApp
adapters against the new trait/type surface so cargo check + release
build are green.

## src/channels/whatsapp_web.rs

- BotBuilder gained a 4th typestate slot (with_runtime). Use
  `wa_rs::TokioRuntime` to satisfy it; enable wa-rs's `tokio-runtime`
  feature on our optional dep.
- with_device_props rewritten to take a `DevicePropsOverride` builder
  (oxidezap/whatsapp-rust#586) instead of three positional Option args.
- Event handlers now receive `Arc<Event>` (PR zeroclaw-labs#613); switched the match
  to `match &*event` so variant-data binding still works.
- `Client::get_phone_number_from_lid` was removed in favor of the
  unified `Client::get_lid_pn_entry(&Jid) -> Result<Option<LidPnEntry>>`
  (zeroclaw-labs#487); call site rewritten to extract `entry.phone_number`, error
  branch swallowed back to None to preserve best-effort enrichment
  semantics.
- `Client::upload` gained a third `UploadOptions` arg; pass
  `UploadOptions::default()` for legacy behavior.
- `UploadResponse` cryptographic fields are now `[u8; 32]` (was
  Vec<u8>); use the `*_vec()` accessors to convert before populating
  protobuf message fields.
- `Client::send_message` now returns `SendResult { message_id, to }`
  rather than a bare String (PR zeroclaw-labs#597); log via `.message_id`.
- `Bot::run()` returns `BotHandle` (Future + abort handle wrapper);
  field type updated from `tokio::task::JoinHandle<()>` accordingly.

## src/channels/whatsapp_storage.rs

- `to_store_err!` macro now wraps errors as
  `Box<dyn Error + Send + Sync>` to match the new
  `StoreError::Database(Box<dyn Error>)` variant (was
  `StoreError::Database(String)`).
- `SignalStore::load_identity` returns `Option<[u8; 32]>` (was
  `Option<Vec<u8>>`); validates length and copies into a fixed array.
- `SignalStore::get_session` and `SignalStore::load_prekey` return
  `Option<bytes::Bytes>`; added `bytes` as a feature-gated direct dep
  and converted via `Bytes::from(vec)`.
- Added `SignalStore::get_max_prekey_id` (queries MAX(id), 0 on empty)
  and `AppSyncStore::get_latest_sync_key_id` (most-recent key by
  key_id desc) — both new in wa-rs-core 0.5.
- ProtocolStore's per-device sender-key tracking replaces the legacy
  SKDM model. Removed `get_skdm_recipients` /
  `add_skdm_recipients` / `clear_skdm_recipients` /
  `mark_forget_sender_key` / `consume_forget_marks` (no longer trait
  members) and added `get_sender_key_devices`,
  `set_sender_key_status`, `clear_sender_key_devices`,
  `delete_sender_key_device_rows`, `clear_all_sender_key_devices`.
- Added `ProtocolStore::delete_devices` (force re-fetch on next
  query).
- Added the sent-message retry trio: `store_sent_message`,
  `take_sent_message` (atomic SELECT+DELETE under an immediate tx),
  `delete_expired_sent_messages`. The expiry path is correct but the
  daemon doesn't yet schedule a cleanup cron — flagged with a
  TODO(wa-rs-0.5) comment so the table doesn't grow unbounded.
- `DeviceListRecord` gained a `raw_id: Option<u32>` field (ADV identity
  index); `update_device_list` / `get_devices` round-trip it via a new
  `raw_id` column.

## SQLite schema

- New tables: `sender_key_devices` (per-device SKDM status),
  `sent_messages` (retry payload store).
- `device_registry.raw_id` column added; legacy databases get the
  column via an idempotent `ALTER TABLE ADD COLUMN` migration probed
  through `pragma_table_info` (SQLite has no IF NOT EXISTS for ADD
  COLUMN).
- Legacy `skdm_recipients` and `sender_key_status` tables remain in
  place but are no longer read or written; left in to avoid touching
  active production session DBs.

## Cargo.toml

- Pinned `bytes = "1"` behind the whatsapp-web feature to satisfy the
  new `Bytes` return types in storage traits.
- Enabled wa-rs's `tokio-runtime` feature so `wa_rs::TokioRuntime` is
  in scope for the BotBuilder.

## Stubs / TODOs

- `delete_expired_sent_messages` works correctly but no daemon cron
  invokes it yet — TODO comment in the impl.

## Validation

- `cargo check --release --features whatsapp-web,observability-otel`:
  clean.
- `cargo build --release --features whatsapp-web,observability-otel`:
  clean (6m 06s).
- `cargo test --release -p zeroclawlabs --features whatsapp-web` —
  storage suite: 3 / 3 passing
  (`rusqlite_store_creates_database`,
  `lid_mapping_round_trip_preserves_learning_source_and_updated_at`,
  `delete_expired_tc_tokens_returns_deleted_row_count`).

Refs: docs/plans/2026-05-06-001-wa-rs-rebrand-plan.md

* fix(channels/whatsapp): address code-review findings on storage impl

- Drop unreachable Err(QueryReturnedNoRows) arm in get_max_prekey_id
  (MAX(...) aggregates always return one row, NULL on empty).
- Use i64::from(r) instead of `r as i64` for u32 -> i64 widening.
- Replace `r as u32` with u32::try_from(r).ok() on raw_id round-trip
  so out-of-range DB values degrade cleanly rather than truncating
  silently. raw_id drives session/sender-key invalidation; a wrong
  value would falsely reset live sessions.
- take_sent_message: switch from default `transaction()` (BEGIN DEFERRED)
  to `transaction_with_behavior(Immediate)` for true SELECT+DELETE
  atomicity at the SQLite layer. Process-level Mutex<Connection> already
  serialized the body, but the immediate semantics match upstream's
  reference impl and protect against future shared-connection scenarios.

All net-new clippy errors from #40aee96 cleared. Two pre-existing
needless_borrow lints remain on whatsapp_web.rs and are out of scope.

Refs: docs/plans/2026-05-06-001-wa-rs-rebrand-plan.md

* chore(deps): pin wa-rs* fork to commit SHA, not branch

[patch.crates-io] now uses rev = "46322bdccd2f91751215a6bbe93c7331b19e84ec"
instead of branch = "wa-rs-rebrand-2026-05" so the resolved tree is
reproducible across builds.

Pinned commit corresponds to:
- 46322bd refactor(wacore): replace if-let-guard with bare boolean guard
- a5ea93c fix(build): correct over-eager rename in waproto/build.rs doc
- c8c3e36 refactor: rebrand crates from whatsapp-rust to wa-rs namespace

Refs: docs/plans/2026-05-06-001-wa-rs-rebrand-plan.md

* style: cargo fmt auto-format on whatsapp adapters

Pre-push quality gate caught formatting drift introduced during the
wa-rs 0.5 adaptation. Mechanical reformatting only — no behavioral
changes.

* fix(cron): gate whatsapp delivery arm on whatsapp-web feature

Pre-existing bug: the cron scheduler match arm for whatsapp delivery
used WhatsAppWebChannel without gating on the whatsapp-web feature,
breaking builds with --no-default-features (or any feature combo
that excludes whatsapp-web).

Mirrors the existing matrix-channel pattern: real impl gated on
feature=enabled, no-op fallback bails with a clear error when the
feature is disabled.

Caught by pre-push quality gate. Pre-existing on main; bundled here
since the PR already touches whatsapp surfaces.

* fix(tools/git_operations): clear GIT_DIR/GIT_WORK_TREE before git subprocess calls

Pre-existing issue surfaced by pre-push hook: when zeroclaw runs git
operations from a context where GIT_DIR is set in the environment
(e.g., inside a git hook), the inherited env overrides the
caller-specified path and git operates on the wrong repo.

This tool's whole purpose is to run git on a path the caller specified,
so we explicitly clear GIT_DIR and GIT_WORK_TREE before each git
spawn — both in the production run_git_command helper and in the test
fixtures that bootstrap temp repos.

Caught when pushing the wa-rs rebrand branch: 3 tests
(git_operations_work_in_subdirectory, blocks_readonly_mode_for_write_ops,
rejects_unknown_operation) failed under the hook env but passed when
run directly. Reproduced and fixed.

* test(channels/whatsapp): add atomicity + raw_id round-trip tests

Addresses P1 findings from the expert-panel review of PR #52:

1. take_sent_message atomicity — verifies SELECT+DELETE under Immediate
   transaction returns the payload exactly once. Second call to take on
   the same key returns None. Guards against silent loss of retry-receipt
   payloads.

2. DeviceListRecord raw_id round-trip — verifies Some(u32) and None
   both survive the i64 conversion in update_device_list / get_devices.
   Catches regressions where a wrong cast would falsely invalidate live
   sessions per the rationale in bf841ec.

Refs: docs/plans/2026-05-06-001-wa-rs-rebrand-plan.md

---------

Co-authored-by: Test <test@test.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants