# Where a change belongs `wacore` owns protocol, crypto, typed IQs and platform-neutral storage/runtime traits. It builds for WASM and ESP32; do not introduce Tokio there. `waproto` contains buffa-generated protobufs. `src/features` exposes the Tokio SDK. Host tools under `tools/` stay outside published runtime dependencies. Find the behavior in WhatsApp Web before changing protocol logic. Follow [wa_web_reference.md](wa_web_reference.md), change the owning layer, and add a regression at the boundary that can observe the behavior. ## State and I/O Keep protocol decisions synchronous and pass time as data. Connection health and maintenance cadence live in `wacore::protocol::health`; reconnect timing and retry session policy live in their sibling modules. The SDK owns timers, random jitter, task cancellation and execution of the resulting actions. `wacore::socket::frame_encoder` owns write nonces and retirement after a failed write. The socket driver owns batching, backpressure, CPU offloading and transport completion. A failed write may have reached the peer and must retire its key. `wacore::request::pending` owns request registration generations. SDK guards translate future cancellation into guarded removal. Disconnect clears entries without resetting their generation sequence. App-state preparation consumes owned wire data and produces uncommitted results without accessing storage. Publish patch state and mutations only after derived effects and the cursor/MAC commit succeed. History parse jobs own their compressed input and policy snapshot; their driver retains admission and blocking-work leases. `tests/sans_io_boundary.rs` guards these modules against executor, I/O, randomness and global-clock dependencies. Use explicit timestamps in their tests so independent simulations can run without replacing process-wide providers. ## Protocol types Use `ProtocolNode::try_from_node_ref` as the parser and `IqSpec` to pair a request with its response. Constructors borrow `&Jid`; SDK callers use `client.execute(Spec::new(&jid))`. Node helpers in `wacore/src/iq/node.rs` keep missing-field errors consistent. See `groups.rs` and `blocklist.rs` for complete implementations. Child-bearing nodes need explicit implementations. Streaming responses implement `IqStreamSpec`; their streaming and tree parsers must agree. Extend the existing encode fast path instead of adding another. `WireEnum` owns serde and the wire spelling. Do not derive serde separately. Tagged enums generate `Tag`; dispatch on that type. Keep validation and protocol limits in the request type. Public event payloads are non-exhaustive, builder-constructed, and use `Option` for absent data. Read the stability policy on `Event` in `wacore/src/types/events.rs` before extending it. ## Ownership and persistence Read device state through `get_device_snapshot`. Publish mutations through `DeviceCommand` and `PersistenceManager::process_command`, or scoped `modify_device_async`; direct write-lock mutation bypasses the snapshot. Signal encryption/decryption uses per-address locks. Incoming chat lanes serialize processing, including offline commit ordering. Outgoing sends do not use a chat lock. Advance and return Signal state before publishing ciphertext; `confirm_signal_state_pre_wire` must succeed when a lease gate is pending. Never replace the batch-safe flush with a raw cache flush during offline drain. Session/sender-key reservations are exclusive ceilings for one chain. Only confirmed backend writes release the matching version's dirty state and gate. Failed writes, tombstones, checked-out state and cancellation remain pending. A clean reload in the same cache incarnation is exact; a new incarnation burns the persisted lease. An old database restore is not detectable from its bytes. Consumed prekeys are deleted only after their promoted session is durable. See `wacore/src/store/signal_cache.rs`, `wacore/libsignal/src/protocol/state/session.rs` and `wacore/libsignal/src/protocol/sender_keys.rs` for lock order, cancellation guards and recovery tests. SQLite's default NORMAL mode establishes transaction/process-crash ordering; FULL is configurable for stronger storage durability. Do not promise power-loss survival under NORMAL. Noise selection and certificate trust live in `src/handshake.rs` and `wacore/noise`. Transient errors must not invalidate trusted certificates; crypto-fatal IK errors clear the cache. An explicit fixture bypass cannot mint trusted cache provenance or authorize IK. Keep transport TLS configs scoped to one intended trust/tenant policy; cloning an Arc-backed config shares its store. ## Optional subsystems A subsystem can leave the core when entry is through an existing dispatch key, its state is read only by itself, its core requirements already have independent callers, and its removal does not change fields, builder setters or variants of public types. `EventKind` indices are persisted interest bits; preserve them. Keep subsystem state together under `src/client/subsystem.rs`, using weak references where ownership would otherwise form a cycle. The executable guards are `tests/subsystem_boundary.rs` and `wacore/tests/voip_control_boundary.rs`. Native plugins use the existing registration and lifecycle hooks. WASM keeps plugin dependencies disabled. Do not expand core state for a single plugin or claim protocol parity from an application hook. SDK integration guides belong in https://github.com/oxidezap/whatsapp-rust-docs. ## Synchronization and recovery `wacore::sync_engine` owns app-state scope admission, bootstrap ownership, reservation equivalence, retry budgets and partial-result reconciliation. It also owns history-sync admission, receipt timing and generation accounting. Keep these decisions executor-free. SDK adapters own atomic publication, locks, queue capacity, host callbacks and storage operations. Check app-state scope admission before starting a write. Once an apply has committed its cursor, publish its mutations even if that scope retired during the write. History-sync receipt timing differs for ordinary delivery, capture and durable replay. Preserve those boundaries when adding adapter effects. ## Public contract changes Public modules, re-exports, generated models, macro output and dependency types in signatures carry source compatibility obligations, including behind features. Use the SDK's dependency re-exports to avoid version mismatches. Do not remove public paths or change construction without updating isolated consumers and providing a migration. Runtime, transport, HTTP, storage, durability hooks and neutral media ports remain open implementation points. Required trait methods and tighter thread bounds break implementers. Add defaults only when absence has an honest, safe meaning; never replace atomic persistence with sequential writes or silently accept an unsupported capability. Batch overrides inherit the owning method's contracts. ## Session transitions and host completions `wacore::session_engine` owns lifecycle transitions, IQ exchange stages, retry policy stages and prepared Signal output. Feed it explicit time and completion inputs. Keep transport, executor and storage calls in the SDK adapters. The optional `session-simulator` feature exposes replay helpers; it is not linked into default consumers. The SDK keeps the lifecycle model behind one mutex and publishes its atomic projection for frequent reads. Worker generations retire background work; transport tickets identify physical connections and survive authentication's generation increment. Cleanup must validate its physical owner after waiting for serialization. A cancelled reader loses its frame decoder, so a restarted supervisor retires that connection before dialing again. `SignalCommitOutcome` is a trusted fact reported by the persistence adapter. The core orders preparation, confirmation and publication but cannot verify I/O. The SDK must obtain the outcome from the global, batch-safe persistence gate before exposing prepared output. Keep the durable-lease fast path and per-address Signal locks intact. Empty live-delivery bookkeeping releases burst capacity when a lane retires after its idle timeout. Active and replayable reservations keep their data. The receive host samples its instance clock once per read and starts a cold decoder burst after a ten-second gap. That boundary releases an empty accumulator's old backing; partial frames keep their bytes and normal reassembly behavior. Instance-clock clients bind the same clock allocation to runtime and storage. Validate alignment after wrapping runtimes, and forward clock ports through runtime decorators. Built-in virtual clocks drive their own sleeps and wake on monotonic advancement. A clock without a timer port must advance with its inner executor. Public legacy time helpers retain process-clock behavior; instance-aware SDK paths supply explicit time or the selected provider.