fix(channels/whatsapp): restore Apr-2026 protocol parity via whatsapp-rust 0.6 + namespace revert (#6246) - #6706
Conversation
Bumps WhatsApp Web crate constraints from `wa-rs*` 0.2 to the upstream-published `whatsapp-rust`/`wacore`/`waproto` family at 0.6 (oxidezap/whatsapp-rust, on crates.io 2026-05-11). Restores protocol parity after the WhatsApp Web server-side change ~2026-04-24 documented in zeroclaw-labs#6246. Mechanical name map (per discussion on zeroclaw-labs#6246): wa-rs -> whatsapp-rust wa-rs-core -> wacore wa-rs-binary -> wacore-binary wa-rs-proto -> waproto wa-rs-tokio-transport -> whatsapp-rust-tokio-transport wa-rs-ureq-http -> whatsapp-rust-ureq-http-client Source imports renamed in the two channel adapters (`whatsapp_web.rs`, `whatsapp_storage.rs`); textual doc-comment mentions left as-is for a cosmetic follow-up. The `tokio-runtime` feature is enabled on `whatsapp-rust` to satisfy the 4-slot BotBuilder typestate added in oxidezap/whatsapp-rust#586. Build is intentionally broken at this commit -- the next commit patches `whatsapp_web.rs` and `whatsapp_storage.rs` for the breaking deltas surfaced by the 0.2 -> 0.5 jump (~109 compile errors expected: Arc<Event> wrapping from zeroclaw-labs#613, BotBuilder typestate from zeroclaw-labs#586, removed methods, fixed-size crypto arrays, SendResult typed return, StoreError::Database boxed errors). Refs: - zeroclaw-labs#6246 (root issue) - oxidezap/whatsapp-rust#487, zeroclaw-labs#586, zeroclaw-labs#597, zeroclaw-labs#601, zeroclaw-labs#613 (breaking changes) - #52, #55 (validated downstream on the rebrand fork)
The 0.2 → 0.6 jump across multiple oxidezap/whatsapp-rust release cycles broke the WhatsApp Web channel adapter. This commit rebuilds the web-side adapter against the new trait/type surface (storage adapter follows in the next commit). - BotBuilder gained a 4th typestate slot (with_runtime). Use `whatsapp_rust::TokioRuntime` to satisfy it; the `tokio-runtime` feature was already enabled on the optional dep in the prior commit. - `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. Refs: zeroclaw-labs#6246, oxidezap/whatsapp-rust#487, zeroclaw-labs#586, zeroclaw-labs#597, zeroclaw-labs#613. Validated downstream on #52 + #55.
The whatsapp-rust 0.6 adapt commit (e8e34c3) dropped Box::pin on the text send path while keeping it on the voice and media paths. The send_message future is large (~34KB) on all three sites; dropping the wrap at one site risks tripping clippy::large_futures once the storage adapter compiles and clippy can run. Restore Box::pin parity with the voice/media call sites and the reference fork commit (40aee96).
Rebuilds the RusqliteStore backend against the new wacore 0.6 trait/type surface so the WhatsApp Web feature compiles cleanly. - `to_store_err!` macro now wraps errors as `Box<dyn Error + Send + Sync>` to match the new `StoreError::Database(Box<dyn Error + Send + Sync>)` 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 convert 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 wacore 0.6. - 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`, plus getter/cleaner — new persistence surface from wacore 0.6. - DeviceListRecord raw_id round-trip — `Some(u32) <-> i64` conversion preserved (catches regressions where a wrong cast would falsely invalidate live sessions). - Schema migrations for the new sender-key-devices and sent-messages tables; backward-compatible additive ALTERs. - Fixed the transitive \`prost::Message\` import that lost its re-export under wacore 0.6 by widening the \`prost\` direct dep's feature gate to include \`whatsapp-web\` (previously gated only to \`channel-lark\`). Refs: zeroclaw-labs#6246, oxidezap/whatsapp-rust#487, zeroclaw-labs#586, zeroclaw-labs#597, zeroclaw-labs#613. Validated downstream on #52 + #55.
clippy::needless_borrow tightened in rust-1.93 and now flags two sites in whatsapp_web.rs where we passed &T into a fn already expecting &T (so the compiler immediately auto-derefs): - extract_mentioned_jids takes &Message, we passed &msg where msg is already &Message via the Arc<Event>-wrapped event arm - render_pairing_qr takes &str, we passed &code where code is already &String via the same event pattern Validated downstream on #55.
The per-entry INSERT...ON CONFLICT loop in set_sender_key_status could leave partial state if a panic or connection drop interrupted the batch: some (group, device) pairs flipped to has_key=true and others not. That silently breaks SKDM resend logic, since the device-list cache no longer matches reality. Mirror the immediate-transaction pattern already used in take_sent_message so the whole batch either commits or rolls back together.
`delete_expired_sent_messages` filters on (device_id, created_at) but the table only has a primary-key index on (chat_jid, message_id, device_id), so the cleanup pass full-scans the table. With the cleanup cron not yet wired, `sent_messages` grows unbounded and the scan cost rises with it. Add a covering composite index so the cleanup remains cheap once it is scheduled. `IF NOT EXISTS` keeps schema-init idempotent across restarts.
`init_schema` previously ran the CREATE TABLE batch and the conditional `ALTER TABLE device_registry ADD COLUMN raw_id` as separate statements on a raw connection. A crash or SIGKILL between them (or a transient `database is locked` from a launchd start-up race) left the DB with the new tables but no `raw_id` column. On next boot the PRAGMA probe sees the column missing and re-runs the ALTER — which works in the happy path, but if it failed permanently the partial-migration state stuck. Move the PRAGMA probe ahead of the transaction (it's read-only, and on a fresh DB it correctly observes no table → skip ALTER), then open a transaction around the CREATEs, new sent_messages index, and the conditional ALTER. The whole migration now commits or rolls back as a unit.
Fixes shipped in the follow-up
Tests green (1044 passed, 0 failed). Deferred to follow-up issuesOut of scope for a wa-rs version bump. Items 1–2 will be filed as separate issues; 3–7 are for a future cleanup pass.
NotePre-existing on
|
CI status — need toolchain guidanceQuality Gate on Root cause
Same constraint the fork hit onboarding wacore 0.5 in Micra-io#52. The fix there was patching
Three paths to green
Path 1 is my preference. Path 2 is the safe fallback if the MSRV bump is unwelcome. Say the word and I'll push the fix. Pinging @theonlyhennygod @singlerider @JordanTheJet since this touches the toolchain pin and the channel:whatsapp work you've been tracking. |
|
@alexandme updating CI versions in this PR is a no-go. You may have more luck running |
|
@alexandme I checked the live PR state, the failed lint job, #6246, and @singlerider's toolchain comment. I agree with @singlerider that bumping ZeroClaw's CI/MSRV in this PR would make the scope too broad. For this PR, please keep ZeroClaw on the current CI/toolchain contract and use a compatible dependency path instead: either a patched/pinned |
|
Read both comments, won't touch CI here. Plan applying the pinned wacore source option you mentioned:
Will ping for re-review once CI Lint is green with the evidence and retention details. |
The published wacore 0.6.0 crate uses an `if let` guard at `wacore/src/history_sync.rs:93` that requires Rust 1.94+; ZeroClaw CI pins 1.93.0, so the workspace fails to compile against the crates.io release. Temporarily pinning the six WA crates (whatsapp-rust, wacore, wacore-binary, waproto, whatsapp-rust-ureq-http-client, whatsapp-rust-tokio-transport) to upstream commit 9734fb2 — the head of oxidezap/whatsapp-rust#632 which rewrites the guard for 1.93 compatibility. This is a plain `git` source dep, not a [patch.crates-io] shim: package names already match upstream, so the workspace deps just point at the upstream repo. CI Lint expected to flip green here. A follow-up commit on this PR will swap these git refs back to versioned crates.io deps once upstream publishes the fix as 0.6.1. Refs: oxidezap/whatsapp-rust#632, zeroclaw-labs#6246.
|
Ready for re-review.
Will swap the git refs back to versioned crates.io deps in a follow-up commit on this PR once upstream releases the fix (likely 0.6.1). |
Audacity88
left a comment
There was a problem hiding this comment.
@alexandme I reviewed current head 54e82a0be2ea23fb765467edfa57bff265eaccf8 against #6246, the current PR body, the green CI run, the changed WhatsApp storage/web adapter code, and the pinned whatsapp-rust 0.6 storage contract. I agree with the direction and the MSRV-compatible temporary git pin, but I think one storage migration gap still blocks this from safely restoring WhatsApp Web sessions.
🟢 What looks good — The protocol dependency path and cleanup story are much closer now
The move to the upstream 0.6 crate family looks aligned with the April protocol break, and the temporary 9734fb2 pin is a reasonable way to avoid bundling a Rust/MSRV bump into this PR. I also checked the sent-message retention path: upstream’s keepalive loop does call delete_expired_sent_messages on a periodic cadence when the default TTL is nonzero, so the added sent_messages(device_id, created_at) index is the right shape for that cleanup path.
🔴 Blocking — Persist the full 0.6 device state before relying on existing sessions
The custom RusqliteStore schema/save/load path in this PR still does not add or round-trip the new 0.6 Device fields. In the pinned 0.6 upstream contract, several additional fields are persistent device state, including next_pre_key_id, server_has_prekeys, nct_salt, server_cert_chain, and login_counter. Upstream has explicit sqlite migrations for at least next_pre_key_id and login_counter, and treats the rest as persistent Device fields rather than runtime-only data.
That means an existing ZeroClaw WhatsApp session can connect, save updated 0.6 device state, then lose those fields on the next daemon restart. The loss is not just cosmetic: next_pre_key_id is the monotonic pre-key counter, server_cert_chain enables cached Noise IK reconnects, and login_counter is explicitly sent on login as persisted anti-abuse state. This undercuts the PR’s upgrade/session-reuse claim and leaves restart behavior different from the upstream 0.6 storage backend.
Please add the missing device columns/migrations to the custom schema, persist them in DeviceStore::save, reload them in DeviceStore::load, and add at least one restart-style round-trip test that proves those 0.6 fields survive through RusqliteStore. Once that is in place, I think the remaining review can focus on final smoke evidence and the branch refresh.
Round-trip the 5 new Device fields introduced in whatsapp-rust 0.6 (next_pre_key_id, server_has_prekeys, nct_salt, server_cert_chain, login_counter) through RusqliteStore. Without this, daemon restart silently resets the monotonic pre-key counter and anti-abuse login counter, and drops the cached Noise IK server cert chain — undermining session reuse claimed by the upstream-mirror PR. server_cert_chain serializes as JSON inside a BLOB column for schema tolerance. Column types and defaults mirror upstream's sqlite-storage migrations so a future cross-store migration tool is trivial. Addresses CHANGES_REQUESTED on zeroclaw-labs#6706.
Mirrors the existing needs_raw_id pragma-probe pattern for the 5 new device columns added in the previous commit. Probe runs before the transaction; conditional ALTER TABLE ... ADD COLUMN statements run inside the same transaction as the CREATEs so a crash between them rolls back cleanly. Existing on-disk WhatsApp session databases (including ~/.zeroclaw/whatsapp-session.db on the deployed daemon) upgrade in-place on the next RusqliteStore::new() call.
|
Verified at pin
Two new tests:
Also smoke-tested on a deployed daemon with a real pre-0.6 session DB. Behavior matched the spec:
|
|
@alexandme Thanks for the detailed follow-up here. I saw the new device-state persistence commits and the extra pre-0.6 migration/test evidence. Before I do the next re-review pass, could you refresh this branch against current |
Refresh PR zeroclaw-labs#6706 against current master per reviewer request (zeroclaw-labs#6706 comment by @Audacity88). Conflicts resolved in crates/zeroclaw-channels/src/whatsapp_web.rs: - recipient_to_jid JID parse: kept master's structured zeroclaw_log Reject event on parse failure, with the wacore_binary::jid::Jid type from this branch. - send() text path: kept this branch's Box::pin + SendResult.message_id for the whatsapp-rust 0.6 send_message return shape, with master's zeroclaw_log::record! DEBUG event (replacing the tracing::debug! call). - listen() prelude: combined this branch's whatsapp-rust / wacore / wacore_binary / whatsapp_rust_tokio_transport / whatsapp_rust_ureq_http_client imports with master's Arc<alias> capture (used downstream by the per-message attribution sites added in zeroclaw-labs#6398). - PairingQrCode event arm: kept master's structured zeroclaw_log::record! for the QR-received notice; kept this branch's `render_pairing_qr(code)` call site (whatsapp-rust 0.6's `match &*event` already binds `code` as a reference, so no explicit `&` is needed). - Took master's removal of the dead-code WIP attachment scaffold (normalize_incoming_content / send_wa_attachment / WaAttachmentKind / WaAttachment / parse_attachment_markers / mime_from_path / wa_media_type / find_matching_close). These were all `#[allow(dead_code)] // WIP: not yet wired into send path` and were deliberately dropped in zeroclaw-labs#6398; preserving them would expand this PR's scope. No callers outside the WIP block. Validation: - cargo check -p zeroclaw-channels --features whatsapp-web: clean. - cargo test -p zeroclaw-channels --features whatsapp-web --locked: 1128 passed, 0 failed (plus 4 in proof_orchestrator_session_context). - cargo fmt --all: clean.
|
@Audacity88 Refreshed against Merge (not rebase) per @singlerider's note on #6009. Single conflict file: Resolution choices:
Validation on the merge commit: Ready for the next re-review pass. |
|
@alexandme Thanks for the update here. I checked the current live PR state, and GitHub is still reporting the branch as conflicting/dirty against Could you refresh against current |
Refresh against current master (d05c8a9) to clear PR zeroclaw-labs#6706 dirty state. Single conflict: crates/zeroclaw-channels/Cargo.toml — the `whatsapp-web` feature definition. Upstream still references the `wa-rs*` rebrand names; this PR carries the canonical `whatsapp-rust` / `wacore` / `waproto` / `whatsapp-rust-*` transports plus `dep:bytes` (required by wacore 0.6 storage traits for `Bytes` return types in SignalStore::get_session / load_prekey). Resolved in favor of HEAD. Cargo.lock auto-merged cleanly with only the canonical crate entries.
|
@Audacity88 Refreshed against current Validation on the merge commit:
|
Audacity88
left a comment
There was a problem hiding this comment.
@alexandme I re-reviewed current head f6a36b7 against #6246, my earlier changes-requested review, the latest refresh comments, the current WhatsApp storage/web diff, the green CI run, and GitHub's current BEHIND mergeability state. The blocker I raised on the 0.6 device-state migration is addressed.
✅ Resolved — wacore 0.6 device state now survives restart
The current head adds the five missing Device fields to the fresh device schema and the pre-0.6 migration path, writes them in DeviceStore::save, reloads them in DeviceStore::load, and covers both a restart-style save/load round trip and a legacy 18-column table migration/reopen path. That closes my concern that an upgraded WhatsApp Web session could connect once and then lose persisted pre-key, server-cert, salt, or login-counter state on the next daemon restart.
🟢 What looks good — the refresh kept the dependency recovery scoped
The later master-refresh commits kept the PR centered on the WhatsApp Web protocol recovery: the 0.6 crate-family pin remains explicit and MSRV-compatible, the current branch drops the WIP attachment scaffold that master already removed, and the merge conflict resolution preserves the structured logging and alias attribution changes from master while carrying the new whatsapp-rust / wacore type surface. CI is green on the refreshed head, and the latest author validation comment covers the current merge commit.
No blocking findings from me on this reviewed head. Before merge, this still needs the normal branch-refresh/mergeability pass because GitHub currently reports the PR as BEHIND, and I’ll clean up the stale maintainer labels separately.
…whatsapp-rust 0.6 + namespace revert (#6246) (#6706) - 8c25871 chore(deps)!: bump wa-rs* family to upstream whatsapp-rust 0.6 - e8e34c3 fix(channels/whatsapp): adapt whatsapp_web.rs to whatsapp-rust 0.6 - 0b82469 fix(channels/whatsapp): Box::pin send_message text path for parity - 6dc949d fix(channels/whatsapp): adapt whatsapp_storage.rs to wacore 0.6 - 6e1e164 fix(channels/whatsapp): drop needless borrows surfaced by rust-1.93 - f4406d3 style: apply cargo fmt to whatsapp adapters - 577aaa7 fix(channels/whatsapp): wrap set_sender_key_status loop in a transaction - a633dc5 perf(channels/whatsapp): index sent_messages(device_id, created_at) - e80b71b fix(channels/whatsapp): make schema init atomic across migrations - 54e82a0 chore(deps): pin whatsapp-rust family to upstream fix commit 9734fb2 - 90d1859 fix(channels/whatsapp): persist wacore 0.6 device fields - 5a62df7 fix(channels/whatsapp): migrate pre-0.6 device tables to add 0.6 columns - 2ef4e97 Merge upstream/master into feat/wa-rs-revert-rebrand-to-upstream - f6a36b7 Merge upstream/master into feat/wa-rs-revert-rebrand-to-upstream - 3de8f9c Merge branch 'master' into feat/wa-rs-revert-rebrand-to-upstream fa51898
…-rust 0.6 + namespace revert (zeroclaw-labs#6246) (zeroclaw-labs#6706) - 8c25871 chore(deps)!: bump wa-rs* family to upstream whatsapp-rust 0.6 - e8e34c3 fix(channels/whatsapp): adapt whatsapp_web.rs to whatsapp-rust 0.6 - 0b82469 fix(channels/whatsapp): Box::pin send_message text path for parity - 6dc949d fix(channels/whatsapp): adapt whatsapp_storage.rs to wacore 0.6 - 6e1e164 fix(channels/whatsapp): drop needless borrows surfaced by rust-1.93 - f4406d3 style: apply cargo fmt to whatsapp adapters - 577aaa7 fix(channels/whatsapp): wrap set_sender_key_status loop in a transaction - a633dc5 perf(channels/whatsapp): index sent_messages(device_id, created_at) - e80b71b fix(channels/whatsapp): make schema init atomic across migrations - 54e82a0 chore(deps): pin whatsapp-rust family to upstream fix commit 9734fb2 - 90d1859 fix(channels/whatsapp): persist wacore 0.6 device fields - 5a62df7 fix(channels/whatsapp): migrate pre-0.6 device tables to add 0.6 columns - 2ef4e97 Merge upstream/master into feat/wa-rs-revert-rebrand-to-upstream - f6a36b7 Merge upstream/master into feat/wa-rs-revert-rebrand-to-upstream - 3de8f9c Merge branch 'master' into feat/wa-rs-revert-rebrand-to-upstream
…-rust 0.6 + namespace revert (zeroclaw-labs#6246) (zeroclaw-labs#6706) - 8c25871 chore(deps)!: bump wa-rs* family to upstream whatsapp-rust 0.6 - e8e34c3 fix(channels/whatsapp): adapt whatsapp_web.rs to whatsapp-rust 0.6 - 0b82469 fix(channels/whatsapp): Box::pin send_message text path for parity - 6dc949d fix(channels/whatsapp): adapt whatsapp_storage.rs to wacore 0.6 - 6e1e164 fix(channels/whatsapp): drop needless borrows surfaced by rust-1.93 - f4406d3 style: apply cargo fmt to whatsapp adapters - 577aaa7 fix(channels/whatsapp): wrap set_sender_key_status loop in a transaction - a633dc5 perf(channels/whatsapp): index sent_messages(device_id, created_at) - e80b71b fix(channels/whatsapp): make schema init atomic across migrations - 54e82a0 chore(deps): pin whatsapp-rust family to upstream fix commit 9734fb2 - 90d1859 fix(channels/whatsapp): persist wacore 0.6 device fields - 5a62df7 fix(channels/whatsapp): migrate pre-0.6 device tables to add 0.6 columns - 2ef4e97 Merge upstream/master into feat/wa-rs-revert-rebrand-to-upstream - f6a36b7 Merge upstream/master into feat/wa-rs-revert-rebrand-to-upstream - 3de8f9c Merge branch 'master' into feat/wa-rs-revert-rebrand-to-upstream
Summary
masterBumps the WhatsApp Web crate family from
wa-rs*0.2 to upstreamwhatsapp-rust/wacore/waproto0.6 (oxidezap/whatsapp-rust, published to crates.io 2026-05-11). The 0.6 release portstulir/whatsmeow@74a8496, the post-2026-04-24 protocol fix. Upstreamwa-rs*0.2 (homun-app fork) is dead: zero issues, zero PRs, last commit 2026-02-20.Renames ZeroClaw imports from the
wa_rs*aliases back to the upstream package names. Drops thewa-rs*alias and the original[patch.crates-io]shim. While the upstream MSRV fix oxidezap/whatsapp-rust#632 is in-flight,whatsapp-rust/wacore/waprotoand the two transports pin to upstream commit9734fb2via a plaingitsource — not a[patch.crates-io]shim, since package names already match upstream. A follow-up commit on this PR will swap the git pin back to the versioned crates.io release (likely 0.6.1) once upstream publishes it. Delivers step 5 of the plan I posted on [Bug]: WhatsApp Web channel: pair succeeds but messages don't flow after April 2026 server-side protocol bump #6246, tracked at Micra-io/zeroclaw#53.ZeroClaw-side adaptations against the 0.2 → 0.6 trait/type surface, mapped to upstream PRs where the change is directly attributable:
get_lid_pn_entry(&Jid)replacesget_phone_number_from_lidentry.phone_number; error branch maps toNoneto preserve best-effort enrichmentDevicePropsOverridebuilder + BotBuilder 4-slot typestatewith_device_props; addedwhatsapp_rust::TokioRuntimefor thewith_runtimeslot; enabledtokio-runtimefeature on the depClient::send_messagereturnsSendResult { message_id, to };StoreError::Database(Box<dyn Error>);Bytesinstead ofVec<u8>in store-trait returns.message_id;to_store_err!wraps errors asBox<dyn Error + Send + Sync>; convert viaBytes::from(vec); addedbytesto thewhatsapp-webfeature gateArc<Event>match &*eventBotBuilder/BotHandleshape changes (Bot::run returnsBotHandle, Future + abort)tokio::task::JoinHandle<()>toBotHandle; newProtocolStoremethods (per-device sender-key tracking, sent-message retry trio,delete_devices,raw_idround-trip) backing the new trait surfaceAdds
bytesandprostto thewhatsapp-webfeature gate. Both were transitively re-exported bywa-rs-core0.2;wacore0.6 no longer surfaces them.--features whatsapp-web(the only consumer of these deps). Other channel features unaffected. Storage schema gains two additive tables and one additive column; legacy SKDM tables stay in CREATE for backward compatibility.risk: medium,size: M,dependencies,channel,channel:whatsapp.Validation Evidence (required)
cargo fmt --all -- --check:cargo clippy --all-targets --features whatsapp-web -- -D warnings:The single clippy error is pre-existing on
master, not introduced by this PR. I confirmed viagit checkout upstream/master -- crates/zeroclaw-providers/src/openai_codex.rsand re-ran clippy: same error, same line, same source. Introduced in #6117 (commit6f96122ab, 2026-05-11) when rust-1.95 tightenedclippy::collapsible_match. This PR does not touch the file (git diff a090cbce3..HEAD -- crates/zeroclaw-providers/src/openai_codex.rsis empty).cargo test --features whatsapp-web -p zeroclaw-channels --locked(the crate this PR modifies):cargo build --release --features whatsapp-web:Binary size 16,486,704 bytes (was 15,654,384 on PR #55's wa-rs-aliased build, +830 KB).
bytesandprostbecame direct deps, and the lockfile gainedcipher 0.5,aes 0.9,ctr 0.10,hkdf 0.13,cbc, andblock-paddingfromwacore's transitive graph.Commands run and tail output: all four commands from the template, above.
Beyond CI — what did you manually verify?
daemon_state.json: allstatus: "ok"). Config loaded withUnknown config key ignoredwarnings for fork-local keys (mention_only,allowed_groups,mention_name), which is correct lenient-deserialization behavior on upstream master. Rolled back to the prior binary immediately; post-rollback state matches pre-deploy.wacore0.6 source: pair handshake surviveszeroclaw service restart, inbound and outbound roundtrip on a live WhatsApp account,zeroclaw channel doctorflips ❌ → ✅. The protocol code is identical between that PR and this one; only the namespace names and lockfile differ.Branch-specific smoke evidence (2026-05-17, in-place swap on the production daemon):
Built locally from worktree at HEAD
54e82a0be(the git-rev pin commit) withcargo build --release --features whatsapp-web. Binary SHA256:9b48ba677ad38478223e8f8c5acf09aad1efb222fea5dcbea5bc65fe416e9f28, 16,503,296 bytes. Swapped in over the production binary (old SHAbd9bc8d166beceaac1fdcfdd121e003735a0481795121fc200bf91a8de188a16preserved at~/.cargo/bin/zeroclaw.bak-pre6706).Note: pre-deploy production was on
wa-rs*0.2 with WhatsApp broken since the 2026-04-24 protocol break (the original bug from [Bug]: WhatsApp Web channel: pair succeeds but messages don't flow after April 2026 server-side protocol bump #6246). The smoke test verifies the new build fixes it.status: okindaemon_state.jsonzeroclaw service stop+service start(restart test)T_startonward → 0 hits)channel doctorreports❌ WhatsApp unhealthy (auth/config/network)on both runs (before and after restart), despite the bot demonstrably exchanging messages with a live WhatsApp account during the same window. This is a stale-check side-finding — the doctor's WA Web probe doesn't match the newwacore 0.6connection state. Worth a follow-up inzeroclaw-channels'schannel doctorimpl, but unrelated to the wire protocol or this PR's correctness.Old session DB (paired before 2026-04-24) was not loadable by the new client — the daemon emitted fresh QR codes until paired. This is the same upgrade path consumers of the
wa-rs*→whatsapp-rustjump should expect.Skipped commands: full-workspace
cargo test. The storage and channel changes are confined tozeroclaw-channels; the clean release-build link covered cross-crate behavior.Security & Privacy Impact (required)
~/.zeroclaw/whatsapp-session.db) with two additive tables and one additive column.bytes::Byteswhere they used to returnVec<u8>. This is a Rust-side ergonomic shift, not a storage-format change.Compatibility (required)
!marking the breaking change.masterand runcargo update -p whatsapp-rust -p wacore -p waproto. Or justcargo build --release --features whatsapp-webto let Cargo regenerate the lockfile from scratch.RusqliteStore::ensure_schemapath: additive tablessender_key_devicesandsent_messages, additive columndevice_registry.raw_id. No manual SQL required.whatsapp-session.db. If it was paired during the broken window (between 2026-04-24 and this fix), re-pair fromWhatsApp → Linked Devices.Rollback (required for
risk: mediumandrisk: high)git revert <merge-sha>followed bycargo build --release --features whatsapp-webputs the build back onwa-rs*0.2. A daemon binary swap reverses the change for an existing install. The schema additions are non-destructive: the new tables and column stay in the DB, unused by the older client.whatsapp-webcargo feature is the single gate. Building without it produces a daemon with no WhatsApp Web channel, matching the pre-fix behavior when the feature was off.zeroclaw channel doctorreports❌ WhatsApp unhealthy (auth/config/network)after pair.zeroclaw channel sendfails withWhatsApp Web client not connected. Initialize the bot first.Connectedevent.sent_messagesretentionThe new
sent_messagestable backs WA Web's retry flow: when we send a message, the serialized payload is stored keyed by(chat_jid, message_id, device_id)so a retry-receipt from the recipient can trigger a re-encrypt + resend without re-rendering the message.Retention parameters (wacore 0.6 defaults, unchanged):
sent_message_ttl_secs = 300(5 minutes) atwhatsapp-rust/src/cache_config.rs:259.delete_expired_sent_messages(cutoff)every ~12 keepalive ticks (≈ 5 minutes wall-clock) atwhatsapp-rust/src/keepalive.rs:151-163.now_secs() - sent_message_ttl_secs.ZeroClaw SQLite-side implementation:
crates/zeroclaw-channels/src/whatsapp_storage.rs:287-294:(chat_jid, message_id, device_id)primary key,payload BLOB,created_at INTEGER.crates/zeroclaw-channels/src/whatsapp_storage.rs:1244-1253:DELETE FROM sent_messages WHERE created_at < ?1 AND device_id = ?2.idx_sent_messages_device_created ON sent_messages(device_id, created_at)was added by commita633dc5eaon this branch specifically so the cleanup query stays O(log n). Without it the periodic DELETE would full-scan the table.Growth bound: at the default 300s TTL and a sustained 100 msgs/sec, the table holds ~30K rows × payload size (typically 200B–2KB serialized), so single-digit MB worst case. Keepalive-driven cleanup keeps the row count bounded; the index keeps the cleanup itself cheap.
Failure modes:
whatsapp_webchannel restart paths indaemonre-spawn the keepalive task on reconnect.SQLITE_BUSYduring cleanup just retries on the next ~5min tick. Entries are short-lived by design, so no durability impact.No new config keys:
sent_message_ttl_secscomes fromCacheConfig's default. Surfacing it on the ZeroClaw[channels.whatsapp]block is out of scope here.Refs: #6246, Micra-io#52/#53/#55, oxidezap/whatsapp-rust#487/#586/#597/#613/#621/#624/#627/#629/#632.