Skip to content

refactor(sqlite-storage)!: replace bincode with prost for persisted blobs - #911

Merged
jlucaso1 merged 15 commits into
mainfrom
claude/remove-bincode-prost
Jun 28, 2026
Merged

refactor(sqlite-storage)!: replace bincode with prost for persisted blobs#911
jlucaso1 merged 15 commits into
mainfrom
claude/remove-bincode-prost

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

What

Removes the bincode dependency from whatsapp-rust-sqlite-storage and models the three persisted BLOB columns with protobuf via prost derive macros (no .proto file) instead.

The SQLite backend serialized three blobs with bincode:

blob column type
server cert chain device.server_cert_chain CachedServerCertChain
app-state sync key app_state_keys.key_data AppStateSyncKey
app-state hash state app_state_versions.state_data HashState

Why

  • Duplicated capability. The workspace already serializes with prost (the wire protocol) and serde_json (e.g. GroupInfo, Device). bincode was the only serializer here that wasn't one of those, for 3 tiny cold blobs.
  • Fragile on disk. bincode is positional and not self-describing, so a struct/field change can silently misdecode persisted rows. prost is field-tagged and tolerant of additive changes.
  • Smaller dep tree. Drops bincode, bincode_derive and virtue. prost was already a workspace dependency, so no new external crate is added. (Binary-size CI: net smaller, -4 deps.)

How

  • New wire module in the storage crate with four #[derive(Clone, PartialEq, prost::Message)] structs and encode_*/decode_* helpers.
  • Conversions happen only at the storage boundary; the wacore domain types are untouched (the fixed-size [u8; 32] cert key and [u8; 128] hash state become bytes on the wire, with length checks on decode). The app-state sync key decode also rejects non-32-byte key material so a bincode row that happens to parse as protobuf with garbage isn't accepted.

Handling existing (bincode) data — self-healing, no migration

Old rows are bincode-encoded and can't be decoded as protobuf. Rather than a destructive migration, the transition self-heals lazily:

  • Decode is tolerant: an undecodable blob (old bincode row or genuine corruption) is treated as absent — None for a sync key, HashState::default() for a version, None for the cert chain.
  • Missing keys are requested before processing: process_app_state_sync_task now checks get_missing_key_ids before decoding a batch; if a referenced key isn't in the backend (e.g. a now-absent old row), it sends an AppStateSyncKeyRequest, waits briefly for the primary to re-share, and re-fetches. This runs before patch/snapshot processing (which would otherwise fail on the missing key with an untyped error), uses the real key ids, and is independent of error types. Key-share receipt now notifies on every share, so repairing multiple keys works.

Net effect after upgrade: app-state keys are re-requested and overwritten as protobuf, app-state versions re-sync from 0, and the cert chain is rebuilt on the next handshake — without deleting anything.

Tests

  • test_server_cert_chain_survives_save_load_roundtrip (full DB save/reopen/load) exercises the prost path.
  • Round-trip tests in wire for all three types, plus wrong-length rejection tests for the cert key, the hash state, and the sync-key material.
  • cargo clippy --workspace --all-targets and cargo test -p whatsapp-rust-sqlite-storage pass.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR switches sqlite-storage BLOB persistence from bincode to prost-based wire encoding, updates sqlite-store reads and writes to use the new helpers, changes app-state missing-key retry flow, and adjusts key-share notification and mutation-MAC clearing behavior.

Changes

SQLite wire encoding and app-state sync flow

Layer / File(s) Summary
Dependency swap and wire module registration
storages/sqlite-storage/Cargo.toml, storages/sqlite-storage/src/lib.rs
bincode is removed, prost is added, and the new internal wire module is declared in the sqlite-storage crate.
Protobuf wire types and helpers
storages/sqlite-storage/src/wire.rs
Defines protobuf wire structs and fixed-length constants, implements encode/decode helpers for server cert chains, app-state sync keys, and hash state, and adds round-trip plus invalid-length tests.
SqliteStore BLOB call sites switch to wire helpers
storages/sqlite-storage/src/sqlite_store.rs
Device cert-chain, app-state sync-key, and app-state version reads and writes use crate::wire helpers instead of bincode, with decode failures warning and returning absent or default values; related test comments and self-healing corruption tests are updated.
Missing-key retries in app-state sync
src/client/app_state.rs
sync_collections_batched_inner and process_app_state_sync_task add per-iteration guards, preflight missing-key detection, retry-on-repair behavior, and page reset handling; request_missing_keys_with_dedup now returns whether a fresh request was sent.
Key-share wakeup handling
src/message/special.rs
handle_app_state_sync_key_share now stores the received flag idempotently and notifies waiters when stored keys are present, without the previous first-time swap gate.
Genesis mutation-MAC clearing
wacore/src/appstate_sync.rs, src/appstate_sync.rs
AppStateProcessor::process_patch_list clears cached mutation MACs when seeding an empty collection from a genesis patch without a snapshot, and adds a helper that inlines external blobs before checking missing key ids.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#511: Directly overlaps with the app-state missing-key repair flow and request_missing_keys_with_dedup behavior changed here.
  • oxidezap/whatsapp-rust#766: Directly related to the same mutation-MAC clearing logic in wacore/src/appstate_sync.rs.
  • oxidezap/whatsapp-rust#688: Related to the new missing_key_ids_after_inline flow used by the client app-state sync changes.

Suggested labels

api-design

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: replacing bincode with prost for persisted SQLite blobs.
Description check ✅ Passed The description is directly aligned with the changeset and accurately explains the storage codec migration and self-healing behavior.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/remove-bincode-prost

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.

The SQLite backend persisted three BLOB columns (server cert chain,
app-state sync keys, app-state hash state) via bincode. That duplicated
serialization the workspace already has (prost for the wire protocol,
serde_json for other blobs) and bincode's positional format is fragile
for on-disk data across struct changes.

Model the three blobs as protobuf with prost derive macros (no .proto
file) in a new `wire` module, converting at the storage boundary so the
wacore domain types stay untouched. Decode degrades gracefully on an
undecodable blob (mirrors the existing cert-chain handling): the cert
chain and app-state sync keys are treated as absent and re-fetched,
app-state versions reset to re-sync from 0. The bincode->protobuf
transition is therefore self-healing, no destructive migration needed.

Drops the bincode dependency (and bincode_derive, virtue) entirely.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195GwTrq6bA8rc9pLwyqfwb
@jlucaso1
jlucaso1 force-pushed the claude/remove-bincode-prost branch from 478130c to 111c739 Compare June 19, 2026 23:14

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@storages/sqlite-storage/src/wire.rs`:
- Around line 215-241: Add a new test function after
hash_state_default_roundtrips that validates the error handling in
decode_hash_state when provided with a hash of incorrect length. Create a test
named hash_state_rejects_wrong_hash_len that constructs a HashStateWire with a
hash vector that is not 128 bytes long (such as 64 bytes), encodes it, and
verifies that decode_hash_state returns an error rather than silently truncating
or accepting the invalid input. This mirrors the validation pattern at line
150-153 and provides coverage equivalent to the existing
server_cert_chain_rejects_wrong_key_len 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: f9ebcce6-01e2-41be-bfd1-ff90ad7cc549

📥 Commits

Reviewing files that changed from the base of the PR and between c3e44df and 478130c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • storages/sqlite-storage/Cargo.toml
  • storages/sqlite-storage/src/lib.rs
  • storages/sqlite-storage/src/sqlite_store.rs
  • storages/sqlite-storage/src/wire.rs

Comment thread storages/sqlite-storage/src/wire.rs
@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.12 MiB 10.10 MiB -13.62 KiB (-0.13%) 🔽
bin .text 8.17 MiB 8.16 MiB -7.44 KiB (-0.09%) 🔽
bin allocated (text+data+bss) 10.11 MiB 10.10 MiB -9.29 KiB (-0.09%) 🔽
llvm-lines wacore 644,533 644,533 0
llvm-lines wacore copies 17,879 17,879 0
llvm-lines whatsapp-rust lib 656,508 658,431 +1,923 (+0.29%) 🔺
llvm-lines whatsapp-rust lib copies 20,409 20,447 +38 (+0.19%) 🔺
deps crates (Cargo.lock) 469 465 -4 (-0.85%) 🔽
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.48 MiB 1.49 MiB +3.31 KiB (+0.22%) 🔺
.text wacore 526.28 KiB 527.65 KiB +1.37 KiB (+0.26%) 🔺
.text wacore_binary 155.85 KiB 155.84 KiB -8 B (-0.01%) 🔽
.text wacore_libsignal 165.86 KiB 165.86 KiB 0
.text wacore_appstate 143.77 KiB 144.24 KiB +475 B (+0.32%) 🔺
.text wacore_noise 27.71 KiB 27.71 KiB 0
.text waproto 871.99 KiB 871.99 KiB 0
.text whatsapp_rust_sqlite_storage 481.00 KiB 475.07 KiB -5.94 KiB (-1.23%) 🎉
.text whatsapp_rust_tokio_transport 43.67 KiB 43.67 KiB 0
.text whatsapp_rust_ureq_http_client 8.81 KiB 8.81 KiB 0
.text std 1000.91 KiB 995.60 KiB -5.31 KiB (-0.53%) 🔽
.text other deps 3.29 MiB 3.29 MiB -1.27 KiB (-0.04%) 🔽
Top movers (cargo-bloat attribution)
Crate main PR Δ
whatsapp_rust_sqlite_storage 481.00 KiB 475.07 KiB -5.94 KiB (-1.23%)
std 1000.91 KiB 995.60 KiB -5.31 KiB (-0.53%)
whatsapp_rust 1.48 MiB 1.49 MiB +3.31 KiB (+0.22%)
bincode 1.77 KiB (removed) -1.77 KiB (-100.00%)
wacore 526.28 KiB 527.65 KiB +1.37 KiB (+0.26%)

Baseline: c57cef791 (latest main run) · Head: 67a4f2158 · Graphs

Mirrors server_cert_chain_rejects_wrong_key_len so the HashState length
invariant has the same negative coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195GwTrq6bA8rc9pLwyqfwb

@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: 478130c103

ℹ️ 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 storages/sqlite-storage/src/sqlite_store.rs
@codspeed-hq

codspeed-hq Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will improve performance by 15.33%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 178 untouched benchmarks

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation send_message[20] 5.9 ms 5.1 ms +15.33%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/remove-bincode-prost (7fd9f91) with main (c57cef7)

Open in CodSpeed

The bincode->protobuf switch leaves existing app_state_keys,
app_state_versions and device.server_cert_chain rows undecodable. The
app-state sync path hard-fails with KeyNotFound before the missing-key
re-request runs, so a returned-absent key does not self-heal (flagged in
review).

Add an up-only migration that clears those rows so nothing undecodable
remains: keys are re-requested from the primary device, versions re-sync
from 0, and the cert chain is rebuilt on the next handshake. With the
migration owning the transition, revert the app-state decode paths to
propagate decode errors (their original behavior) instead of masking them
as absent/default. The cert-chain decode stays tolerant, as it was before
this PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195GwTrq6bA8rc9pLwyqfwb

@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: 351b50843f

ℹ️ 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 storages/sqlite-storage/migrations/2026-06-19-000000_clear_bincode_blobs/up.sql Outdated

@cubic-dev-ai cubic-dev-ai 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.

3 issues found across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread storages/sqlite-storage/src/sqlite_store.rs Outdated
Comment thread storages/sqlite-storage/src/sqlite_store.rs Outdated
Comment thread storages/sqlite-storage/migrations/2026-06-19-000000_clear_bincode_blobs/up.sql Outdated
Replaces the destructive migration with read-time self-healing, per review:

- App-state sync key / version decode treats an undecodable blob (an old
  bincode row or genuine corruption) as absent / default instead of fatal.
- On KeyNotFound, the app-state sync path now requests the missing key from
  the primary device before retrying, so an absent key is re-shared and the
  next write persists it as protobuf. Previously the request only ran after a
  successful decode, so a missing key never recovered.

Old bincode rows are handled lazily at read with no destructive migration;
the migration added earlier is removed. The cert-chain decode was already
tolerant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195GwTrq6bA8rc9pLwyqfwb

@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: 8a388ea8d4

ℹ️ 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/client/app_state.rs Outdated
Comment thread storages/sqlite-storage/src/wire.rs
Addresses two review findings on the self-heal path:

- decode_app_state_sync_key rejects key material that isn't 32 bytes. An
  old bincode row can occasionally parse as protobuf with garbage key data;
  accepting it would derive bad sub-keys and fail later with MAC errors
  instead of being treated as absent and re-requested.

- On KeyNotFound the retry arms the key-share listener before sending the
  request and always waits for the share, and every key share now notifies
  (not just the first). Repairing multiple missing keys now works instead of
  only the first collection recovering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195GwTrq6bA8rc9pLwyqfwb

@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: 404072fdc9

ℹ️ 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/client/app_state.rs Outdated

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

Caution

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

⚠️ Outside diff range comments (1)
src/client/app_state.rs (1)

91-103: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add diagnostic logging when base64 decode fails.

Look, if the key ID is malformed and we can't decode it, we need to know about it. Right now if STANDARD_NO_PAD.decode(id_b64) fails, we silently skip the wait and continue - no visibility into what went wrong. For a billion-user platform, silent failures are not acceptable.

🔧 Proposed fix to add diagnostic logging
                         use base64::Engine as _;
-                        if let Ok(key_id) =
-                            base64::engine::general_purpose::STANDARD_NO_PAD.decode(id_b64)
-                        {
+                        match base64::engine::general_purpose::STANDARD_NO_PAD.decode(id_b64) {
+                        Ok(key_id) => {
                             let listener = self.initial_keys_synced_notifier.listen();
                             self.request_missing_keys_with_dedup(vec![key_id]).await;
                             debug!(target: "Client/AppState", "App state key missing for {:?}; requested it, waiting up to 10s for key share then retrying", name);
                             if rt_timeout(&*self.runtime, Duration::from_secs(10), listener)
                                 .await
                                 .is_err()
                             {
                                 warn!(target: "Client/AppState", "Timeout waiting for key share for {:?}; retrying anyway", name);
                             }
                         }
+                        Err(decode_err) => {
+                            warn!(target: "Client/AppState", "Failed to decode missing key ID for {:?}: {decode_err}", name);
+                        }
+                        }
                         continue;
🤖 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/client/app_state.rs` around lines 91 - 103, The base64 decode operation
for the key ID (STANDARD_NO_PAD.decode(id_b64)) has no error handling, causing
silent failures when the key ID is malformed. Add an else clause to the if let
Ok(key_id) pattern to log a diagnostic warning when the base64 decode fails,
including the malformed id_b64 value and a clear error message. This will
provide visibility into why the key ID could not be processed instead of
silently skipping the key synchronization attempt.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/client/app_state.rs`:
- Around line 91-103: The base64 decode operation for the key ID
(STANDARD_NO_PAD.decode(id_b64)) has no error handling, causing silent failures
when the key ID is malformed. Add an else clause to the if let Ok(key_id)
pattern to log a diagnostic warning when the base64 decode fails, including the
malformed id_b64 value and a clear error message. This will provide visibility
into why the key ID could not be processed instead of silently skipping the key
synchronization attempt.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 84d8a8c2-8011-46fa-b538-a1cfbc25255b

📥 Commits

Reviewing files that changed from the base of the PR and between 351b508 and 404072f.

📒 Files selected for processing (4)
  • src/client/app_state.rs
  • src/message/special.rs
  • storages/sqlite-storage/src/sqlite_store.rs
  • storages/sqlite-storage/src/wire.rs

The KeyNotFound the retry branch caught (AppStateSyncError) is not the error
the decode path returns: a missing key surfaces as AppStateError::KeyNotFound
from lookup_app_state_key and is stringified into an untyped anyhow, so the
re-request never actually fired (raised in review).

Instead, check get_missing_key_ids before process_parsed_patch_list: if the
batch references a key the backend lacks (e.g. an old bincode row that now
decodes as absent), request it, wait briefly for the primary to re-share, and
re-fetch. This uses the real key ids and is independent of error types. The
now-dead base64/request code in the retry branch is reverted to its original
form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195GwTrq6bA8rc9pLwyqfwb

@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: 965fefba2a

ℹ️ 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/client/app_state.rs Outdated
claude and others added 3 commits June 20, 2026 00:34
The task-wide guard suppressed the upfront key request on later paginated
pages, so a page encrypted with a different rotated key (e.g. across upgraded
bincode rows) could hit KeyNotFound without ever requesting it. Reset the
guard after each successfully processed batch so every page gets one repair
attempt, while still bounding a single page to one request+wait cycle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195GwTrq6bA8rc9pLwyqfwb
The wire tests already reject wrong-length protobuf, but nothing exercised the
store boundary that swallows a decode failure into absence. Corrupt the
app-state sync-key and version blobs in the DB and assert get_sync_key reads
None and get_version resets to default (version 0) without erroring -- the
behavior the whole bincode->prost migration relies on.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@jlucaso1 jlucaso1 changed the title refactor(sqlite-storage): replace bincode with prost for persisted blobs refactor(sqlite-storage)!: replace bincode with prost for persisted blobs Jun 28, 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

🤖 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 `@storages/sqlite-storage/src/sqlite_store.rs`:
- Around line 3960-4044: The current test only covers arbitrary corrupt bytes,
but it should verify the legacy no-migration behavior for real old bincode rows.
Update undecodable_blobs_self_heal_to_absent in sqlite_store.rs to insert
hardcoded legacy bincode fixtures for the sync-key and HashState storage paths,
then assert get_app_state_sync_key_for_device and
get_app_state_version_for_device treat them as None/default rather than errors,
and confirm the protobuf setters can overwrite those healed rows afterward.
🪄 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 (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a506ed5f-302a-47b7-87c1-0adc463a2783

📥 Commits

Reviewing files that changed from the base of the PR and between 404072f and 72ae084.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • src/client/app_state.rs
  • storages/sqlite-storage/Cargo.toml
  • storages/sqlite-storage/src/sqlite_store.rs

Comment thread storages/sqlite-storage/src/sqlite_store.rs Outdated
jlucaso1 added 2 commits June 27, 2026 23:07
The missing-key repair waited 10s for a re-share after every request, but the
per-key dedup can suppress the actual send (already asked within its 24h
window). In that case no new re-share is coming from us, so the wait was dead
time on the collection. request_missing_keys_with_dedup now reports whether it
actually sent; the sync loop only waits when it did, otherwise it falls through
and lets the page fail fast so the collection re-syncs on a later attempt --
closer to WA Web's non-blocking orphan-and-retry model.
…rite

Replace the arbitrary-garbage self-heal test with the exact bytes bincode 2.0.1
(config::standard, via serde) produced for AppStateSyncKey and HashState before
the migration. This proves the migration's core property: a real legacy row does
not false-positive as a partially-decoded protobuf -- it reads back as absent /
default, and the protobuf setters then overwrite the healed row. Keeps an
arbitrary-corruption case too.

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

Caution

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

⚠️ Outside diff range comments (1)
src/client/app_state.rs (1)

512-532: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Move the missing-key preflight before external blob downloads.

Right now, a missing key is detected only after Line 457-492 downloads snapshots/mutations; when the request succeeds, Line 531 retries and throws those downloads away. Put this gate immediately after parse_patch_list_ref so large sync payloads don’t get downloaded twice.

🤖 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/client/app_state.rs` around lines 512 - 532, The missing-key preflight in
app_state::sync_page is happening too late, after the external blob download
work has already run, so successful retries discard that downloaded payload.
Move the get_missing_key_ids/request_missing_keys_with_dedup gate to immediately
after parse_patch_list_ref, before the snapshot/mutation fetch path, and keep
the retry/timeout behavior tied to the same sync_page flow so large sync
payloads are not downloaded twice.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/client/app_state.rs`:
- Around line 512-532: The missing-key preflight in app_state::sync_page is
happening too late, after the external blob download work has already run, so
successful retries discard that downloaded payload. Move the
get_missing_key_ids/request_missing_keys_with_dedup gate to immediately after
parse_patch_list_ref, before the snapshot/mutation fetch path, and keep the
retry/timeout behavior tied to the same sync_page flow so large sync payloads
are not downloaded twice.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9a7a8fd4-9561-4460-be85-e8f310e79de1

📥 Commits

Reviewing files that changed from the base of the PR and between 72ae084 and 5bad944.

📒 Files selected for processing (2)
  • src/client/app_state.rs
  • storages/sqlite-storage/src/sqlite_store.rs

@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: 5bad944ac1

ℹ️ 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/client/app_state.rs Outdated
Comment thread storages/sqlite-storage/src/sqlite_store.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.

Caution

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

⚠️ Outside diff range comments (1)
src/client/app_state.rs (1)

434-435: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fix the guard comment to match the reset behavior.

Line 434 says “once per task,” but Line 575 resets the flag after a page decodes, so later pages in the same task can request missing keys again.

Suggested wording
-        // Request the batch's missing keys at most once per task (see below).
+        // Avoid re-requesting the same missing keys while this page is stuck;
+        // reset after a page decodes so later key rotations can repair.
         let mut requested_missing_keys = false;

As per coding guidelines, “When adding code comments, explain why not what, and keep them concise.”

🤖 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/client/app_state.rs` around lines 434 - 435, Update the guard comment
near requested_missing_keys in app_state::AppState::request_batch so it matches
the actual behavior: it is not limited to once per task because the flag is
reset after each page decode. Rewrite the comment to concisely explain why the
guard exists and that it only suppresses duplicate requests within a page decode
cycle, referencing the requested_missing_keys reset path to keep the wording
aligned with the code.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/client/app_state.rs`:
- Around line 434-435: Update the guard comment near requested_missing_keys in
app_state::AppState::request_batch so it matches the actual behavior: it is not
limited to once per task because the flag is reset after each page decode.
Rewrite the comment to concisely explain why the guard exists and that it only
suppresses duplicate requests within a page decode cycle, referencing the
requested_missing_keys reset path to keep the wording aligned with the code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bd810e5a-06ba-4968-941e-4637ee946840

📥 Commits

Reviewing files that changed from the base of the PR and between 5bad944 and 1fd6fe6.

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

@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: 1fd6fe68f9

ℹ️ 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/client/app_state.rs Outdated
An upgraded paired companion (its persisted bincode app-state rows now decode as
absent) failed to re-sync: the self-heal re-request never reached the primary
and the snapshot key was never requested. Three root causes, found by running
the demo against a real migrated DB:

- Target the PRIMARY for the key request. request_app_state_keys sent the peer
  message to device_snapshot.pn, which carries OUR device number, so it
  encrypted to ourselves (no self-session) and failed with 'session not found'.
  Address it to to_non_ad() (device 0), the key source we hold a session with
  from pairing -- mirrors whatsmeow's ownID.ToNonAD().

- Preflight missing keys before processing in BOTH sync paths (the batched
  server_sync path only requested AFTER process_patch_lists, which aborts with
  KeyNotFound first; the single path requested before download). Inline the
  external blobs FIRST via the new missing_key_ids_after_inline: the snapshot's
  key_id lives inside its blob, so checking before download misses it and the
  snapshot still fails. Only wait when a fresh request actually went out (the
  per-key dedup may suppress it).

- Clear stale mutation MACs when seeding from a genesis patch. A version blob
  that reset to 0 keeps its pre-reset MACs; a v1 genesis patch served without a
  snapshot would anchor its ltHash to those stale index->value entries. The
  snapshot path already clears; this covers the no-snapshot genesis path.

Tests: real external-snapshot key visibility after inline, genesis-patch MAC
clear, plus the store-level legacy-bincode self-heal already added.
@jlucaso1
jlucaso1 force-pushed the claude/remove-bincode-prost branch from de76576 to 3862b64 Compare June 28, 2026 02:49

@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: 3862b647a5

ℹ️ 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 storages/sqlite-storage/src/sqlite_store.rs
…nc key

Outbound chat-action mutations encrypt with the latest app-state sync key
(get_latest_sync_key_id -> build_patch / send_app_state_patch). The query
returned the lexicographically-highest key_id without decoding it, so on an
upgraded DB a stale legacy-bincode row that sorts higher (and was never repaired
because no incoming patch referenced it) would be selected, then fail later in
get_app_state_key with KeyNotFound -- breaking a pin/mute/archive push. Load the
candidates in desc order and return the first whose blob actually decodes.

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/app_state.rs`:
- Around line 594-610: `request_keys_and_wait` currently treats a deduped
missing-key request as “nothing to do,” which lets callers decode immediately
and re-hit `KeyNotFound` for already-missing keys. Update the
`AppState::request_keys_and_wait` flow so it distinguishes between sent,
deduped, and failed outcomes from `request_missing_keys_with_dedup`, and make
the deduped path still wait briefly and trigger a refetch before proceeding.
Ensure the callers that rely on this helper continue to use the returned boolean
to decide whether to refetch.
🪄 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 (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 858fa91e-e77a-4a62-920e-b3f65014c185

📥 Commits

Reviewing files that changed from the base of the PR and between de76576 and 9fabcd3.

📒 Files selected for processing (4)
  • src/appstate_sync.rs
  • src/client/app_state.rs
  • storages/sqlite-storage/src/sqlite_store.rs
  • wacore/src/appstate_sync.rs

Comment thread src/client/app_state.rs Outdated

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

ℹ️ 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/client/app_state.rs Outdated
…quest

request_keys_and_wait returned true just because the request was sent, ignoring
the timeout/notification result. A delayed share (or a wake from an UNRELATED
key share) then let the callers process with the row still absent -> KeyNotFound;
and since the per-key request was stamped for 24h, the deduped retries returned
false and skipped the wait entirely, so the collection never recovered.

Now the helper waits even when the dedup suppressed the send (an earlier request
may still be in flight) and RE-VERIFIES every requested key is actually stored
before returning true. The callers drop the once-per-task guard and skip the
collection gracefully when keys are still missing (it re-syncs on a later cycle)
instead of aborting with KeyNotFound. Addresses Codex P1 + CodeRabbit.

@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: 268723731a

ℹ️ 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/client/app_state.rs Outdated
}
if !missing_all.is_empty() && !self.request_keys_and_wait(missing_all).await {
warn!(target: "Client/AppState", "app-state key(s) still missing after request; skipping this batch, will retry on a later sync");
return Ok(());

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 Keep critical collections pending when key repair times out

When the requested app-state key share does not arrive within the 10s wait, this returns Ok(()) from the batched sync even though none of the pending collections were processed. The initial critical-sync path in src/client/node_io.rs treats sync_collections_batched(...) success as permission to dispatch Connected, so an upgraded device with missing/corrupt persisted keys can skip CriticalBlock/CriticalUnblockLow state without scheduling a retry until some unrelated dirty/server sync happens. Return an error or keep these collections pending instead of reporting success.

Useful? React with 👍 / 👎.

The previous commit returned Ok(()) from sync_collections_batched when the
re-shared key didn't arrive in 10s. The initial critical-sync path in node_io
treats that Ok as permission to cancel its 180s retry watchdog and dispatch
Connected, so an upgraded device with corrupt persisted keys would surface
Connected with CriticalBlock/CriticalUnblockLow unsynced and no scheduled
retry. Return an error from both sync paths instead, so the caller keeps the
watchdog (or logs and retries on the next server_sync) rather than treating the
collection as synced. Already-repaired keys/pages stay persisted, so the retry
resumes cheaply. Addresses Codex P1.

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/client/app_state.rs

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

ℹ️ 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/client/app_state.rs
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