Skip to content

feat: add AB props cache and privacy tokens conditional - #442

Merged
jlucaso1 merged 4 commits into
mainfrom
feat-ab-props-handling
Mar 26, 2026
Merged

feat: add AB props cache and privacy tokens conditional#442
jlucaso1 merged 4 commits into
mainfrom
feat-ab-props-handling

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Mar 26, 2026

Copy link
Copy Markdown
Collaborator

Introduce an in-memory AbPropsCache and populate it from PropsResponse on
connect. Add well-known AB prop config codes and consult the cache to auto-attach privacy tokens on group create and participant add. Add a new AddParticipantsIq to serialize per-participant tokens and export the ab_props store module.

Closes #435

Summary by CodeRabbit

  • New Features

    • Group creation and participant-add flows now support per-participant privacy tokens when enabled by experiment flags.
    • Added an in-memory A/B configuration cache and new experiment flags to control privacy token behavior.
  • Improvements

    • A/B property fetch/apply ordering adjusted to keep in-memory and persisted props consistent.
  • Tests/Chores

    • Connection startup timing refined to better handle handshake vs. connected sequencing.

Introduce an in-memory AbPropsCache and populate it from PropsResponse
on
connect. Add well-known AB prop config codes and consult the cache to
auto-attach privacy tokens on group create and participant add. Add a
new AddParticipantsIq to serialize per-participant <privacy> tokens and
export the ab_props store module.
@coderabbitai

coderabbitai Bot commented Mar 26, 2026

Copy link
Copy Markdown

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds an in-memory A/B props cache and wires it into Client props fetching (apply responses before persisting hashes). Replaces AddParticipantsIq to carry per-participant options (including optional privacy bytes) and updates group create/add flows to conditionally attach tc_token-derived privacy based on A/B flags.

Changes

Cohort / File(s) Summary
A/B Props Cache
wacore/src/store/ab_props.rs, wacore/src/store/mod.rs
New AbPropsCache: async RwLock-backed map with seeded flag; supports apply_response (full replace vs delta merge), get, and is_enabled truthiness checks; unit tests added.
A/B Config Codes
wacore/src/iq/props.rs
Added config_codes module exporting privacy-related config constants (PRIVACY_TOKEN_ON_ALL_1_ON_1_MESSAGES, PRIVACY_TOKEN_ON_GROUP_CREATE, PRIVACY_TOKEN_ON_GROUP_PARTICIPANT_ADD, PRIVACY_TOKEN_ONLY_CHECK_LID).
Client integration
src/client.rs
Added pub(crate) ab_props: Arc<wacore::store::ab_props::AbPropsCache> and accessor; Client::new_with_cache_config initializes cache; fetch_props() gates delta usage on ab_props.is_seeded() and calls ab_props.apply_response(&response).await before persisting response.hash.
Group IQ & Participant Options
wacore/src/iq/groups.rs
Replaced macro-generated AddParticipantsIq (Vec<Jid>) with explicit AddParticipantsIq { group_jid, participants: Vec<GroupParticipantOptions> }; serialization now conditionally includes <privacy> child and omits phone_number for non-LID JIDs; response parsing unchanged; tests updated/expanded.
Group features: token attachment
src/features/groups.rs
create_group() and add_participants() now conditionally attach per-participant privacy from tc_tokens when AB flags enabled; added helpers: resolve_participant_tokens, attach_tokens_to_participants, only_check_lid, resolve_token_key, lookup_valid_token.
Tests / Startup wait
tests/e2e/src/lib.rs
Refactored connect startup wait into two-phase logic: treat PairSuccess as possibly arriving without Connected, allow additional startup sync wait before failing; adjusted timeouts and draining of late Connected event.

Sequence Diagram(s)

sequenceDiagram
    participant Server
    participant Client
    participant Cache as AbPropsCache
    participant Storage as TokenStore

    Server->>Client: PropsSpec Response (full or delta)
    Client->>Cache: apply_response(&response)
    Cache->>Cache: update/merge experiment props
    Cache-->>Client: seeded / values available
    Client->>Server: persist props hash

    Note over Client,Cache: Later: Group operation triggered
    Client->>Cache: is_enabled(PRIVACY_TOKEN_ON_GROUP_CREATE)
    Cache-->>Client: true/false
    alt enabled
        Client->>Storage: lookup_valid_token(participant_jid)
        Storage-->>Client: tc_token or None
        Client->>Client: attach_tokens_to_participants()
    end
    Client->>Server: CreateGroupIq / AddParticipantsIq (with options)
Loading
sequenceDiagram
    participant Client
    participant Cache as AbPropsCache
    participant Storage as TokenStore
    participant Server

    Client->>Cache: is_enabled(PRIVACY_TOKEN_ON_GROUP_PARTICIPANT_ADD)
    Cache-->>Client: true/false
    alt enabled
        Client->>Client: resolve_participant_tokens(&jids)
        loop per participant
            Client->>Storage: lookup_valid_token(jid / resolved key)
            Storage-->>Client: tc_token or None
            Client->>Client: build GroupParticipantOptions (maybe privacy)
        end
        Client->>Server: AddParticipantsIq::with_options(group_jid, options_vec)
    else disabled
        Client->>Server: AddParticipantsIq::new(group_jid, &jids)
    end
    Server-->>Client: ParticipantChangeResponse
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through props and cached a clue,
Tokens tucked in options, privacy too.
I nudged the client, asked the cache to store,
Groups now whisper secrets—soft and more.
Thump—A/B hops, and the tokens soar! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: introducing an AB props cache and implementing privacy token conditionals for group operations.
Linked Issues check ✅ Passed The PR addresses issue #435 by implementing privacy token attachment on group create and participant add operations through AB prop configuration, directly solving the 463 error problem.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the AB props cache and privacy token handling for group operations as required by issue #435.

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

✨ 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-ab-props-handling

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.

@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Mar 26, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 1m 21s —— View job


Code Review

  • Read all changed files
  • Analyze code quality, correctness, and style
  • Post review feedback

Overall this is a clean, well-structured PR. The AB props cache design is solid, tests are thorough, and the privacy token integration follows existing patterns. A few observations:

Looks Good

  • AbPropsCache (wacore/src/store/ab_props.rs) — Clean design with async_lock::RwLock, proper Default impl, and comprehensive tests covering full/delta updates, sampling skip, and truthy value checks.
  • AddParticipantsIq hand-written replacement — Good call replacing the macro-generated version to support per-participant <privacy> children. The new() constructor maintains backwards compatibility.
  • Privacy token helpers (src/features/groups.rs:476-552) — resolve_token_key, lookup_valid_token, and attach_tokens_to_participants correctly mirror the existing send.rs tc_token pattern, including LID resolution and expiry checking.
  • Test coverage — Both the cache module and the IQ serialization have solid test coverage.

Suggestions

  1. is_enabled allocates unnecessarily (wacore/src/store/ab_props.rs:60-67)

    value.to_ascii_lowercase() allocates a new String on every call. Since you're only checking three known values, you could use eq_ignore_ascii_case instead:

    pub async fn is_enabled(&self, config_code: u32) -> bool {
        match self.props.read().await.get(&config_code) {
            Some(value) => {
                value == "1"
                    || value.eq_ignore_ascii_case("true")
                    || value.eq_ignore_ascii_case("enabled")
            }
            None => false,
        }
    }

    Fix this →

  2. Sequential token resolution in resolve_participant_tokens (src/features/groups.rs:479-490)

    The loop resolves tokens sequentially — each iteration awaits resolve_token_key then lookup_valid_token. For large participant lists this could be slow. Consider using futures::future::join_all or FuturesUnordered to resolve tokens concurrently. Not a blocker since typical participant lists are small, but worth noting.

  3. ab_props field visibility (src/client.rs:327)

    The field is pub(crate) but you also expose a public getter ab_props() at line 1603. This is fine as-is, but the getter returns &AbPropsCache which exposes get() and is_enabled() to downstream consumers. Just confirming this is intentional — if the cache is meant to be internal-only, the getter could be pub(crate) instead.

  4. Minor: AbPropConfig::try_from_node error handling (wacore/src/iq/props.rs:174-200)

    The fallback path calls both AbProp::try_from_node and SamplingProp::try_from_node, then unwraps the errors with unwrap_or_else. Since both paths already returned Ok or Err, the unwrap_or_else on line 190/193 is dead code (the Err is guaranteed). Not a bug, just slightly misleading — the unwrap_or_else fallback can never fire. This predates this PR though.

Nits

  • The config_codes module (wacore/src/iq/props.rs:37-44) defines PRIVACY_TOKEN_ON_ALL_1_ON_1_MESSAGES but it's unused in this PR. That's fine if it's planned for future use, just flagging it.

Clean PR, good test coverage. The main actionable suggestion is #1 (avoiding the allocation in is_enabled). The rest are minor.

@github-actions

github-actions Bot commented Mar 26, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Branchfeat-ab-props-handling
Testbedubuntu-latest

🚨 1 Alert

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions()Instructions
instructions x 1e3
📈 plot
🚷 threshold
🚨 alert (🔔)
47.13 x 1e3
(+10.09%)Baseline: 42.81 x 1e3
44.95 x 1e3
(104.85%)

Click to view all benchmark results
BenchmarkInstructionsBenchmark Result
instructions
(Result Δ%)
Upper Boundary
instructions
(Limit %)
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled()📈 view plot
🚷 view threshold
6,197.00
(-6.00%)Baseline: 6,592.51
6,922.13
(89.52%)
binary_benchmark::child_iteration_group::bench_get_children_by_tag📈 view plot
🚷 view threshold
524,304.00
(-28.82%)Baseline: 736,616.74
773,447.57
(67.79%)
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled()📈 view plot
🚷 view threshold
20,868.00
(-6.15%)Baseline: 22,235.46
23,347.23
(89.38%)
binary_benchmark::marshal_group::bench_marshal_allocating📈 view plot
🚷 view threshold
98,202.00
(-18.23%)Baseline: 120,102.34
126,107.45
(77.87%)
binary_benchmark::marshal_group::bench_marshal_auto_allocating📈 view plot
🚷 view threshold
98,230.00
(-11.08%)Baseline: 110,467.25
115,990.62
(84.69%)
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating📈 view plot
🚷 view threshold
532,948.00
(-0.12%)Baseline: 533,585.52
560,264.79
(95.12%)
binary_benchmark::marshal_group::bench_marshal_auto_long_string📈 view plot
🚷 view threshold
15,870.00
(-5.19%)Baseline: 16,738.42
17,575.35
(90.30%)
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating📈 view plot
🚷 view threshold
14,715,201.00
(-8.77%)Baseline: 16,129,030.70
16,935,482.23
(86.89%)
binary_benchmark::marshal_group::bench_marshal_exact_allocating📈 view plot
🚷 view threshold
118,358.00
(-22.29%)Baseline: 152,316.52
159,932.34
(74.01%)
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating📈 view plot
🚷 view threshold
534,378.00
(-0.12%)Baseline: 535,005.41
561,755.68
(95.13%)
binary_benchmark::marshal_group::bench_marshal_exact_long_string📈 view plot
🚷 view threshold
17,919.00
(-4.64%)Baseline: 18,789.97
19,729.47
(90.82%)
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating📈 view plot
🚷 view threshold
28,066,347.00
(-23.38%)Baseline: 36,630,627.29
38,462,158.66
(72.97%)
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating📈 view plot
🚷 view threshold
533,387.00
(-0.12%)Baseline: 534,024.52
560,725.74
(95.12%)
binary_benchmark::marshal_group::bench_marshal_long_string📈 view plot
🚷 view threshold
15,843.00
(-7.97%)Baseline: 17,215.41
18,076.18
(87.65%)
binary_benchmark::marshal_group::bench_marshal_many_children_allocating📈 view plot
🚷 view threshold
14,716,627.00
(-8.76%)Baseline: 16,130,119.42
16,936,625.40
(86.89%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer📈 view plot
🚷 view threshold
107,945.00
(-14.54%)Baseline: 126,310.69
132,626.22
(81.39%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer📈 view plot
🚷 view threshold
98,302.00
(-11.07%)Baseline: 110,539.25
116,066.22
(84.69%)
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled()📈 view plot
🚷 view threshold
90,974.00
(-5.75%)Baseline: 96,521.13
101,347.18
(89.76%)
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,378.00
(-3.71%)Baseline: 7,661.90
8,045.00
(91.71%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled()📈 view plot
🚷 view threshold
91,005.00
(-1.98%)Baseline: 92,842.14
97,484.24
(93.35%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,401.00
(+0.40%)Baseline: 7,371.49
7,740.06
(95.62%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled()📈 view plot
🚷 view threshold
106,790.00
(-1.69%)Baseline: 108,627.14
114,058.49
(93.63%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled()📈 view plot
🚷 view threshold
8,913.00
(+0.33%)Baseline: 8,883.49
9,327.66
(95.55%)
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled()📈 view plot
🚷 view threshold
41,989.00
(-8.59%)Baseline: 45,936.36
48,233.18
(87.05%)
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled()📈 view plot
🚷 view threshold
2,717.00
(-4.82%)Baseline: 2,854.48
2,997.20
(90.65%)
binary_benchmark::unpack_group::bench_unpack_compressed📈 view plot
🚷 view threshold
556,092.00
(+2.72%)Baseline: 541,373.97
568,442.67
(97.83%)
binary_benchmark::unpack_group::bench_unpack_uncompressed📈 view plot
🚷 view threshold
771.00
(-0.33%)Baseline: 773.56
812.24
(94.92%)
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data()📈 view plot
🚷 view threshold
27,706,383.00
(+0.02%)Baseline: 27,701,852.36
29,086,944.98
(95.25%)
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message()📈 view plot
🚷 view threshold
5,544,446.00
(-0.07%)Baseline: 5,548,141.16
5,825,548.22
(95.17%)
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session()📈 view plot
🚷 view threshold
175,119.00
(-1.42%)Baseline: 177,647.88
186,530.27
(93.88%)
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session()📈 view plot
🚷 view threshold
175,768.00
(-1.50%)Baseline: 178,435.99
187,357.79
(93.81%)
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users()📈 view plot
🚷 view threshold
17,235,928.00
(-0.25%)Baseline: 17,278,974.23
18,142,922.95
(95.00%)
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender()📈 view plot
🚷 view threshold
298,417.00
(+0.61%)Baseline: 296,616.69
311,447.52
(95.82%)
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message()📈 view plot
🚷 view threshold
12,695,935.00
(+0.79%)Baseline: 12,595,924.01
13,225,720.21
(95.99%)
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution()📈 view plot
🚷 view threshold
719,597.00
(+0.42%)Baseline: 716,609.85
752,440.35
(95.64%)
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions()📈 view plot
🚷 view threshold
🚨 view alert (🔔)
47,129.00
(+10.09%)Baseline: 42,810.35
44,950.86
(104.85%)

libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction()📈 view plot
🚷 view threshold
15,561,842.00
(+0.00%)Baseline: 15,561,755.95
16,339,843.74
(95.24%)
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages()📈 view plot
🚷 view threshold
5,371,271.00
(-2.15%)Baseline: 5,489,548.98
5,764,026.43
(93.19%)
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session()📈 view plot
🚷 view threshold
311,806.00
(-63.83%)Baseline: 862,000.51
905,100.53
(34.45%)
libsignal_benchmark::signature_group::bench_key_generation keygen📈 view plot
🚷 view threshold
2,830,547.00
(+0.20%)Baseline: 2,824,949.48
2,966,196.96
(95.43%)
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message()📈 view plot
🚷 view threshold
3,452,844.00
(-0.58%)Baseline: 3,473,070.87
3,646,724.42
(94.68%)
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message()📈 view plot
🚷 view threshold
124,999,198.00
(-0.27%)Baseline: 125,334,542.68
131,601,269.81
(94.98%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message()📈 view plot
🚷 view threshold
12,002.00
(+1.66%)Baseline: 11,806.11
12,396.42
(96.82%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message()📈 view plot
🚷 view threshold
3,930.00
(+2.70%)Baseline: 3,826.67
4,018.00
(97.81%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended()📈 view plot
🚷 view threshold
87,073.00
(-0.87%)Baseline: 87,833.08
92,224.74
(94.41%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple()📈 view plot
🚷 view threshold
79,022.00
(-1.07%)Baseline: 79,876.02
83,869.82
(94.22%)
reporting_token_benchmark::key_derivation_group::bench_key_derivation📈 view plot
🚷 view threshold
50,375.00
(-1.16%)Baseline: 50,968.48
53,516.90
(94.13%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message()📈 view plot
🚷 view threshold
5,956.00
(+3.47%)Baseline: 5,756.11
6,043.92
(98.55%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message()📈 view plot
🚷 view threshold
2,227.00
(+4.95%)Baseline: 2,121.95
2,228.05
(99.95%)
reporting_token_benchmark::token_calculation_group::bench_token_calculation📈 view plot
🚷 view threshold
21,920.00
(+0.01%)Baseline: 21,916.76
23,012.60
(95.25%)
🐰 View full continuous benchmarking report in Bencher

@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: d58b984867

ℹ️ 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 on lines +39 to +40
if !response.delta_update {
map.clear();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Handle cold-start delta updates as full AB props state

AbPropsCache starts empty on every process start, but Client::fetch_props() still requests deltas when a persisted props_hash exists. In that common reconnect path, apply_response() merges only changed props and does not repopulate unchanged ones, so is_enabled() can incorrectly return false for active flags until a full props refresh arrives. This breaks AB-gated behavior (e.g., group privacy token attachment) after restart even though the server config is enabled.

Useful? React with 👍 / 👎.

@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: 3

🤖 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/client.rs`:
- Around line 1591-1593: The code is applying a delta response into an empty
AbPropsCache via self.ab_props.apply_response(&response).await which causes
unchanged props to disappear after restart; instead detect when the in-memory
AbPropsCache is unseeded/empty and avoid applying a delta: ensure fetch_props()
is called with a full spec (PropsSpec::new()) or otherwise request a full props
response until the AbPropsCache has been populated, or if a delta response is
received while AbPropsCache is empty, discard it and re-request a full props
response before calling AbPropsCache::apply_response; reference
self.ab_props.apply_response, fetch_props(), PropsSpec::new(), AbPropsCache, and
is_enabled() when making this change.

In `@wacore/src/iq/groups.rs`:
- Around line 1166-1187: In build_iq(), normalize the participant records before
serializing so phone_number is stripped for non-LID JIDs the same way
build_create_group_node() does: call normalize_participants(self.participants)
(or the existing normalization helper) and iterate over the normalized list when
building participant Nodes, ensuring phone_number is only emitted for LID JIDs;
update the mapping that builds participant Node attrs to use the normalized
participants and add a regression test that constructs a PN JID participant and
asserts no phone_number attribute is emitted in the produced IQ.

In `@wacore/src/store/ab_props.rs`:
- Around line 55-67: is_enabled currently conflates "prop is false" and "props
not yet loaded", causing callers to proceed incorrectly; change the API to
expose an uninitialized/tri-state instead of folding to false: either (A) change
pub async fn is_enabled(&self, config_code: u32) -> bool to return Option<bool>
(None = not primed, Some(true/false) = value), or (B) add a new pub async fn
is_enabled_opt(&self, config_code: u32) -> Option<bool> and keep is_enabled as a
convenience wrapper. Implement this by introducing a primed flag (e.g.,
self.props_primed or derive primed from a separate AtomicBool) that is set by
fetch_props(), and have is_enabled/_opt check that flag before reading
self.props (the symbols to update: is_enabled, fetch_props, connect, and the
props field). Update callers to handle None (block/refetch) where appropriate.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 0f5d6a11-2f39-41ad-b7ef-fc71544095df

📥 Commits

Reviewing files that changed from the base of the PR and between 0089394 and d58b984.

📒 Files selected for processing (6)
  • src/client.rs
  • src/features/groups.rs
  • wacore/src/iq/groups.rs
  • wacore/src/iq/props.rs
  • wacore/src/store/ab_props.rs
  • wacore/src/store/mod.rs

Comment thread src/client.rs Outdated
Comment on lines +1591 to +1593
// Populate the in-memory AB props cache so features can query prop values.
self.ab_props.apply_response(&response).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't apply delta props onto a cold cache.

The device snapshot only persists props_hash, not the previous prop map. After a process restart, fetch_props() can request a delta with that hash and this call will merge the changed subset into an empty AbPropsCache, so unchanged props disappear and is_enabled() starts returning false for them. Use a full PropsSpec::new() until the in-memory cache has been seeded with a full response, or persist the full prop set alongside the hash.

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

In `@src/client.rs` around lines 1591 - 1593, The code is applying a delta
response into an empty AbPropsCache via
self.ab_props.apply_response(&response).await which causes unchanged props to
disappear after restart; instead detect when the in-memory AbPropsCache is
unseeded/empty and avoid applying a delta: ensure fetch_props() is called with a
full spec (PropsSpec::new()) or otherwise request a full props response until
the AbPropsCache has been populated, or if a delta response is received while
AbPropsCache is empty, discard it and re-request a full props response before
calling AbPropsCache::apply_response; reference self.ab_props.apply_response,
fetch_props(), PropsSpec::new(), AbPropsCache, and is_enabled() when making this
change.

Comment thread wacore/src/iq/groups.rs
Comment thread wacore/src/store/ab_props.rs Outdated
Comment on lines +55 to +67
/// Check if a prop is enabled (truthy).
///
/// Returns `true` if the value is `"1"`, `"true"`, or `"enabled"` (case-insensitive).
/// Returns `false` if the prop is absent, empty, or has any other value.
pub async fn is_enabled(&self, config_code: u32) -> bool {
match self.props.read().await.get(&config_code) {
Some(value) => matches!(
value.to_ascii_lowercase().as_str(),
"1" | "true" | "enabled"
),
None => false,
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Expose an uninitialized state instead of folding it into false.

is_enabled() makes “flag disabled” and “props not fetched yet” indistinguishable. Because fetch_props() is started asynchronously during connect, the first group create/add after reconnect can still see an empty cache and skip privacy tokens, recreating the 463 path. Add a primed bit or a tri-state lookup so callers can block/refetch instead of silently taking the false branch.

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

In `@wacore/src/store/ab_props.rs` around lines 55 - 67, is_enabled currently
conflates "prop is false" and "props not yet loaded", causing callers to proceed
incorrectly; change the API to expose an uninitialized/tri-state instead of
folding to false: either (A) change pub async fn is_enabled(&self, config_code:
u32) -> bool to return Option<bool> (None = not primed, Some(true/false) =
value), or (B) add a new pub async fn is_enabled_opt(&self, config_code: u32) ->
Option<bool> and keep is_enabled as a convenience wrapper. Implement this by
introducing a primed flag (e.g., self.props_primed or derive primed from a
separate AtomicBool) that is set by fetch_props(), and have is_enabled/_opt
check that flag before reading self.props (the symbols to update: is_enabled,
fetch_props, connect, and the props field). Update callers to handle None
(block/refetch) where appropriate.

@jlucaso1 jlucaso1 changed the title feat: add AB props cache and privacy tokens support feat: add AB props cache and privacy tokens conditional Mar 26, 2026
@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Mar 26, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 1m 10s —— View job


Code Review

  • Read all changed files
  • Analyze code quality, correctness, and style
  • Post review feedback

Overall this is a well-structured PR. The second commit (9a1b912) addressed the key cold-start delta issue flagged by reviewers and added the eq_ignore_ascii_case optimization. Here's my review of the current state:

Looks Good

  • Cold-start delta guard (src/client.rs:1563) — The is_seeded() check before requesting a delta is the right fix. Clean and simple.
  • AbPropsCache (wacore/src/store/ab_props.rs) — Good use of AtomicBool for the seeded flag with proper Acquire/Release ordering. The async_lock::RwLock is appropriate for the read-heavy access pattern.
  • AddParticipantsIq hand-written impl — Clean replacement of the macro-generated version. The phone_number guard (p.jid.is_lid()) at line 1160 correctly mirrors normalize_participants() behavior.
  • Concurrent token resolution (src/features/groups.rs:489) — Using futures::future::join_all for parallel resolution is good.
  • Test coverage — Solid tests for cache behavior (full/delta/sampling/truthy), IQ serialization (privacy present/absent), and PN JID phone_number stripping.

Issues

  1. is_enabled still conflates "not loaded" with "disabled" (wacore/src/store/ab_props.rs:58-67)

    If a group create or participant add happens before fetch_props() completes during connect (race window), is_enabled() returns false and privacy tokens are silently skipped. This could still trigger the 463 error this PR is trying to fix. The seeded flag exists but nothing in the caller side checks it.

    Consider either:

    • (a) Having create_group / add_participants check ab_props.is_seeded() and await the props fetch before proceeding, or
    • (b) Changing is_enabled to return Option<bool> so callers can distinguish "unknown" from "disabled"

    The window is small (props are fetched early in connect), so this may be acceptable in practice, but it's worth documenting the assumption that fetch_props() always completes before user-initiated group operations.

    Fix this →

  2. ab_props accessor visibility (src/client.rs:1601)

    ab_props() is pub(crate) which is correct for now. But worth noting: ab_props field on line 327 is also pub(crate), so the getter is redundant — callers can access the field directly. Either make the field private and keep the getter, or remove the getter. Minor consistency nit.

Nit

  • PRIVACY_TOKEN_ON_ALL_1_ON_1_MESSAGES (config code 10518) is defined but unused. Fine if planned for future use, just flagging.

Clean PR with good test coverage. The main consideration is the race window in issue #1 — if fetch_props is guaranteed to complete before any user-facing group operations, this is safe as-is.


View job

@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: 9a1b912880

ℹ️ 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 src/features/groups.rs
Comment on lines +210 to +213
.ab_props()
.is_enabled(wacore::iq::props::config_codes::PRIVACY_TOKEN_ON_GROUP_CREATE)
.await
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Load AB props before deciding privacy-token behavior

create_group now gates privacy-token attachment on ab_props().is_enabled(...), but the AB cache is populated asynchronously in background init (fetch_props is spawned after connect), so this check returns false on a fresh connection until that job completes. In AB-enabled deployments where group create/add requires privacy tokens, the first operations after wait_for_connected() can still be sent without tokens and fail, which undermines the feature this commit adds. Consider treating props fetch as readiness-critical for these flows or using a fallback that attaches a valid token when one exists.

Useful? React with 👍 / 👎.

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

♻️ Duplicate comments (1)
wacore/src/store/ab_props.rs (1)

57-66: ⚠️ Potential issue | 🟠 Major

Don't collapse "unseeded" into false.

fetch_props() still runs asynchronously during startup, so callers can hit this method before the cache is primed. Returning false in that window still makes the first group create/add path skip privacy tokens and can recreate the 463 failure this PR is trying to fix. Expose a tri-state/readiness API here and make the group flows handle the unprimed case explicitly.

Suggested shape
+    pub async fn is_enabled_opt(&self, config_code: u32) -> Option<bool> {
+        if !self.is_seeded() {
+            return None;
+        }
+
+        let props = self.props.read().await;
+        Some(match props.get(&config_code) {
+            Some(value) => {
+                value == "1"
+                    || value.eq_ignore_ascii_case("true")
+                    || value.eq_ignore_ascii_case("enabled")
+            }
+            None => false,
+        })
+    }
+
     /// True when the prop value is truthy (`"1"`, `"true"`, or `"enabled"`).
     pub async fn is_enabled(&self, config_code: u32) -> bool {
-        match self.props.read().await.get(&config_code) {
-            Some(value) => {
-                value == "1"
-                    || value.eq_ignore_ascii_case("true")
-                    || value.eq_ignore_ascii_case("enabled")
-            }
-            None => false,
-        }
+        match self.is_enabled_opt(config_code).await {
+            Some(enabled) => enabled,
+            None => false,
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/store/ab_props.rs` around lines 57 - 66, The current
is_enabled(&self, config_code: u32) -> bool collapses the uninitialized cache
into false; change this to a tri-state/readiness API so callers can detect
"unseeded" vs explicit false: add a new method (e.g., is_enabled_state or
get_prop_state) that returns an enum (SeededTrue | SeededFalse | Unseeded) or
Option<bool> and update code that calls is_enabled (notably the group create/add
flows) to explicitly handle the Unseeded/None case by deferring action until
fetch_props() has completed; keep fetch_props() and props.read() usage but
ensure is_enabled no longer returns false for missing keys and update callers to
check the readiness enum/option before skipping privacy token logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@wacore/src/store/ab_props.rs`:
- Around line 57-66: The current is_enabled(&self, config_code: u32) -> bool
collapses the uninitialized cache into false; change this to a
tri-state/readiness API so callers can detect "unseeded" vs explicit false: add
a new method (e.g., is_enabled_state or get_prop_state) that returns an enum
(SeededTrue | SeededFalse | Unseeded) or Option<bool> and update code that calls
is_enabled (notably the group create/add flows) to explicitly handle the
Unseeded/None case by deferring action until fetch_props() has completed; keep
fetch_props() and props.read() usage but ensure is_enabled no longer returns
false for missing keys and update callers to check the readiness enum/option
before skipping privacy token logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4ac6ab04-5f5e-46a0-a8fa-80f1be33cbdf

📥 Commits

Reviewing files that changed from the base of the PR and between d58b984 and 9a1b912.

📒 Files selected for processing (5)
  • src/client.rs
  • src/features/groups.rs
  • wacore/src/iq/groups.rs
  • wacore/src/iq/props.rs
  • wacore/src/store/ab_props.rs

@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 `@tests/e2e/src/lib.rs`:
- Around line 108-110: Update the stale inline comments to reflect actual
behavior: change wording that currently claims "PairSuccess + Connected" and
that wait_for_startup_sync waits for Connected — instead document that phase 1
accepts either PairSuccess or Connected (i.e., proceeds on either signal) and
that wait_for_startup_sync handles the critical app-state sync phase (not
specifically waiting for Connected). Locate and edit the comment blocks near the
references to PairSuccess/Connected and the wait_for_startup_sync mention
(search for wait_for_startup_sync and the "PairSuccess + Connected" text) and
make the comments accurate and concise.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: bac09271-b127-4160-bf03-3271e736d852

📥 Commits

Reviewing files that changed from the base of the PR and between 9a1b912 and 7469d61.

📒 Files selected for processing (1)
  • tests/e2e/src/lib.rs

Comment thread tests/e2e/src/lib.rs Outdated
@jlucaso1
jlucaso1 merged commit 30afae3 into main Mar 26, 2026
7 of 8 checks passed
@jlucaso1
jlucaso1 deleted the feat-ab-props-handling branch March 26, 2026 04:22

@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: 3198b1b35c

ℹ️ 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 src/features/groups.rs
Comment on lines 258 to +262
.client
.execute(AddParticipantsIq::new(jid, participants))
.await?;
.ab_props()
.is_enabled(wacore::iq::props::config_codes::PRIVACY_TOKEN_ON_GROUP_PARTICIPANT_ADD)
.await
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Block add_participants until AB props are loaded

add_participants gates privacy-token attachment on ab_props().is_enabled(...), but AB props are fetched asynchronously during background init after connect, so this check is false on a fresh session and the method sends AddParticipantsIq::new(...) without tokens. In deployments where participant-add requires privacy tokens, the first add operation right after wait_for_connected() can be rejected even though the feature is enabled server-side. This path needs the same readiness/fallback handling as create-group so uninitialized AB state does not silently disable token attachment.

Useful? React with 👍 / 👎.

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.

Error reporting when adding people to group: 463 error

1 participant