Phase 1 Steps 4-6 (the receive pipeline, minus the App/sync wiring which
lands next).
- chat_message: the in-memory ChatMessage model (direction, kind, txid, cid,
peer zaddr/pubkey, body, timestamp, payload_position). No libsodium.
- chat_store: threads messages by conversation_id and dedups by
(txid, payload_position) so re-scanning the chain never double-inserts.
- chat_service: owns the long-lived chat identity keypair (move-disabled,
wiped on destruction/clear) and the store. ingest() decrypts each Message
(drops undecryptable ones silently — no plaintext/memo logging), passes a
ContactRequest's plaintext through, and threads the result. No-op without an
identity.
Tests: a metadata batch decrypts into a threaded conversation; re-ingest
dedups; a contact request carries through; a wrong identity decrypts nothing;
no-identity ingest is a no-op.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>