Fix Signal send handling and wrap inbound messages

ober

8d98325c42c88855a39c21f18392885137cc38c3

diff --git a/Makefile b/Makefile
index 3ac9c29..56f5f26 100644
--- a/Makefile
+++ b/Makefile
@@ -52,6 +52,7 @@ run-tui: binary tui-shim
 	./$(BIN) tui $(ARGS)
 
 test: binary
+	$(JEXEC) tests/test-send-result.ss
 	./$(BIN) --help >/dev/null && echo "smoke ok"
 
 install: binary
diff --git a/docs/full-port.md b/docs/full-port.md
new file mode 100644
index 0000000..6c46d4d
--- /dev/null
+++ b/docs/full-port.md
@@ -0,0 +1,1399 @@
+# Full Native Signal Port Plan
+
+Status: planning document
+Date: 2026-06-09
+Repository: `jerboa-signal`
+
+## 1. Objective
+
+Build a native Signal backend for `jerboa-signal` that removes the hard dependency on the `signal-cli` Java process while continuing to use upstream Signal cryptography as provided by `libsignal`.
+
+The intended end state is:
+
+- `jerboa-signal` owns the client/service layer that talks to Signal servers.
+- `jerboa-signal` stores account, session, message, contact, group, and sync state in `jsqlite`.
+- Upstream `libsignal` remains the cryptographic and protocol primitive implementation.
+- A small Rust crate exposes the needed `libsignal` operations through a stable C ABI.
+- Jerboa links that Rust crate into the final binary.
+- The existing `signal-cli` backend remains available during migration as a reference backend and fallback until native parity is proven.
+
+This is not a plan to rewrite Signal cryptography in Jerboa. Rewriting `libsignal` would be a much larger and riskier project, and it would create unnecessary security and compatibility risk. The correct split is:
+
+- Rust/libsignal: crypto, Signal protocol primitives, group crypto primitives, zero-knowledge group support, key/session machinery.
+- Jerboa: application orchestration, storage, network transport, UI integration, retries, tracing, migration, business logic.
+
+## 2. Why This Work Exists
+
+The current app uses `signal-cli` as a subprocess or JSON-RPC service. That gives us a working path quickly, but it also means we inherit behavior and packaging issues outside our control.
+
+Recent concrete issues:
+
+- The native Homebrew `signal-cli` build failed during captcha submission because a GraalVM native image could not resolve an internal Kotlin reflection class.
+- Sends could appear successful while not actually delivering unless we inspect per-recipient result payloads carefully.
+- Rate limit, captcha, and challenge behavior is difficult to present correctly when the real service state is hidden behind another CLI.
+- `signal-cli` releases can stop working when Signal changes server behavior.
+- The Java process adds deployment complexity for an otherwise Jerboa-centered app.
+
+The replacement should preserve the parts that are trustworthy and hard to reproduce, especially upstream cryptography, while moving the service/client behavior into code we can debug, trace, and ship as part of this project.
+
+## 3. Source And License Reality
+
+Relevant upstream projects:
+
+- `libsignal`: <https://github.com/signalapp/libsignal>
+- `Signal-Android`: <https://github.com/signalapp/Signal-Android>
+- `signal-cli`: <https://github.com/AsamK/signal-cli>
+
+Important facts:
+
+- `libsignal` is open source and is the official Signal client library. Its core is Rust with bindings for Java, Swift, TypeScript, and other clients.
+- `Signal-Android` is open source and contains current client behavior, service models, and API usage patterns.
+- `signal-cli` is open source and implements an unofficial Java client using Signal libraries and service code patterns.
+- `libsignal` is AGPL-3.0-only at the time this plan was written.
+- `signal-cli` is GPL-3.0-or-later at the time this plan was written.
+- Copying code from `signal-cli` or `Signal-Android` has license and derivative-work implications.
+- Linking `libsignal` into a final binary also has license implications.
+
+This document is technical, not legal advice. Before distributing binaries, confirm the project's license posture and source-distribution obligations.
+
+The preferred implementation style is:
+
+- Use `libsignal` as a normal upstream dependency.
+- Study `signal-cli` and `Signal-Android` for behavior.
+- Avoid copying large bodies of Java/Kotlin code unless we intentionally accept the license consequences.
+- Keep all third-party licenses and exact source revisions documented in the release artifact.
+
+## 4. Current Architecture
+
+Current rough flow:
+
+```text
+TUI / command code
+  -> Jerboa signal modules
+  -> signal-cli subprocess or JSON-RPC daemon
+  -> signal-cli Java service layer
+  -> libsignal / Signal service APIs
+  -> Signal servers
+```
+
+Important current local modules:
+
+- `signal/rpc.ss`: JSON-RPC client calls into `signal-cli`.
+- `signal/rpc-actor.ss`: actor and timeout behavior around RPC calls.
+- `signal/cmd-send.ss`: command-line send path.
+- `signal/tui/main.ss`: interactive TUI path.
+- `signal/send-result.ss`: send result validation and classification.
+- `signal/trace.ss`: low-level tracing and command selection.
+
+Current behavior to preserve:
+
+- Existing TUI interactions.
+- Existing history behavior.
+- Existing command-line send behavior.
+- Send tracing for debugging.
+- Per-conversation local rate limit handling.
+- Captcha challenge presentation and retry.
+- Clear classification of transport failure, rate limit failure, captcha challenge, and per-recipient failure.
+
+## 5. Target Architecture
+
+Target rough flow:
+
+```text
+TUI / command code
+  -> signal/engine.ss
+  -> native Signal backend
+       -> service orchestration in Jerboa
+       -> jsqlite persistent store
+       -> Rust signal-native static library
+            -> upstream libsignal
+       -> HTTP/WebSocket transport
+       -> Signal servers
+```
+
+The engine boundary is important. The UI and command modules should call an abstract backend so we can keep both implementations during migration:
+
+```text
+signal/engine.ss
+  signal-cli-backend
+  native-backend
+```
+
+High-level proposed module layout:
+
+```text
+signal/
+  engine.ss                 ; backend protocol and common result types
+  backend/
+    signal-cli.ss           ; adapter around existing rpc/cmd behavior
+    native.ss               ; adapter around native service implementation
+  native/
+    account.ss              ; local account/device state
+    provision.ss            ; linked-device provisioning
+    service.ss              ; Signal service HTTP/WebSocket calls
+    receive.ss              ; receive queue loop and envelope handling
+    send.ss                 ; message construction, encryption, submission
+    attachments.ss          ; upload/download/verification
+    groups.ss               ; groups v2 and sender key orchestration
+    contacts.ss             ; contacts, profiles, identity names
+    store.ss                ; jsqlite schema and transactions
+    migrations.ss           ; DB migrations
+    ffi.ss                  ; Jerboa bindings to Rust C ABI
+    events.ss               ; normalized app events emitted upward
+    errors.ss               ; native error taxonomy
+native/
+  signal-native/
+    Cargo.toml
+    src/lib.rs              ; C ABI exported functions
+    src/error.rs
+    src/buffer.rs
+    src/store.rs            ; optional Rust-side store abstractions if needed
+    include/signal_native.h
+```
+
+The final binary should link the Rust static library and expose no Java runtime requirement.
+
+## 6. Backend Interface
+
+Create a narrow interface that represents what the application needs, not what `signal-cli` happens to expose.
+
+Initial interface shape:
+
+```scheme
+(backend-name backend)                         ; symbol/string
+(backend-version backend)                      ; structured version metadata
+(backend-ready? backend)                       ; account/device/store usable?
+
+(backend-list-accounts backend)
+(backend-current-account backend)
+(backend-link-device backend options)          ; provisioning flow
+(backend-submit-captcha backend challenge)
+
+(backend-send-message backend request)         ; returns normalized send result
+(backend-receive-once backend options)         ; fetch/decrypt one batch
+(backend-start-receive-loop backend sink opts)
+(backend-stop-receive-loop backend)
+
+(backend-list-contacts backend)
+(backend-list-groups backend)
+(backend-get-conversation backend conversation-id)
+(backend-mark-read backend conversation-id timestamps)
+
+(backend-upload-attachment backend attachment)
+(backend-download-attachment backend pointer)
+```
+
+The existing UI should depend on normalized app-level objects:
+
+- Conversation ID.
+- Recipient address.
+- Message body.
+- Attachments.
+- Timestamp.
+- Send status.
+- Failure reason.
+- Captcha challenge.
+- Rate limit wait.
+- Delivery/read receipts.
+- Decrypted incoming message event.
+
+The UI should not know whether the backend is `signal-cli` or native.
+
+## 7. Native Rust FFI Strategy
+
+Create a Rust crate at `native/signal-native` that depends on a pinned upstream `libsignal` revision or crate version.
+
+Build output:
+
+- `staticlib` for final binary linking.
+- Optional `cdylib` during development for easier local testing.
+- C header generated manually or via `cbindgen`.
+
+Rules:
+
+- Export only `extern "C"` functions.
+- Do not expose Rust structs across the ABI.
+- Use opaque handles for long-lived state.
+- Use explicit allocation and free functions for buffers returned to Jerboa.
+- Return structured status values, not panics.
+- Catch panics at the FFI boundary.
+- Never log keys, plaintext, session blobs, sealed sender certificates, access keys, or attachment plaintext.
+- Pin the upstream version at the start of every phase and include it in test output.
+
+Preferred result convention:
+
+```c
+typedef struct {
+  int32_t code;
+  uint8_t *data;
+  size_t data_len;
+  uint8_t *error_json;
+  size_t error_json_len;
+} SignalNativeResult;
+```
+
+Every FFI call returns either:
+
+- `code == 0` and `data` contains a serialized result.
+- `code != 0` and `error_json` contains a structured error.
+
+Provide one free function:
+
+```c
+void signal_native_result_free(SignalNativeResult result);
+```
+
+The serialized format should initially be JSON for ease of debugging. Once stable, switch hot paths to protobuf or a compact binary encoding only if profiling proves it matters.
+
+Minimum metadata functions:
+
+```c
+SignalNativeResult signal_native_version(void);
+SignalNativeResult signal_native_build_info(void);
+SignalNativeResult signal_native_supported_features(void);
+```
+
+Candidate crypto/session functions:
+
+```c
+SignalNativeResult signal_native_generate_identity_key_pair(void);
+SignalNativeResult signal_native_generate_registration_id(void);
+SignalNativeResult signal_native_generate_pre_keys(uint32_t start, uint32_t count);
+SignalNativeResult signal_native_generate_signed_pre_key(
+  const uint8_t *identity_key_pair, size_t identity_key_pair_len,
+  uint32_t key_id,
+  uint64_t timestamp_ms);
+
+SignalNativeResult signal_native_process_prekey_bundle(
+  const uint8_t *local_store_json, size_t local_store_json_len,
+  const uint8_t *remote_bundle_json, size_t remote_bundle_json_len);
+
+SignalNativeResult signal_native_encrypt_message(
+  const uint8_t *session_json, size_t session_json_len,
+  const uint8_t *plaintext, size_t plaintext_len);
+
+SignalNativeResult signal_native_decrypt_message(
+  const uint8_t *session_json, size_t session_json_len,
+  const uint8_t *ciphertext, size_t ciphertext_len);
+
+SignalNativeResult signal_native_sealed_sender_encrypt(
+  const uint8_t *request_json, size_t request_json_len);
+
+SignalNativeResult signal_native_sealed_sender_decrypt(
+  const uint8_t *request_json, size_t request_json_len);
+```
+
+Candidate group/sender-key functions:
+
+```c
+SignalNativeResult signal_native_create_sender_key_distribution_message(
+  const uint8_t *request_json, size_t request_json_len);
+
+SignalNativeResult signal_native_process_sender_key_distribution_message(
+  const uint8_t *request_json, size_t request_json_len);
+
+SignalNativeResult signal_native_group_encrypt(
+  const uint8_t *request_json, size_t request_json_len);
+
+SignalNativeResult signal_native_group_decrypt(
+  const uint8_t *request_json, size_t request_json_len);
+```
+
+These names are placeholders. The implementation must map them to actual current `libsignal` APIs during the FFI spike.
+
+## 8. Storage Plan With jsqlite
+
+Use `~/mine/jsqlite` as the SQLite replacement. Do not use C SQLite unless the user explicitly changes this decision.
+
+Storage goals:
+
+- Durable account/device state.
+- Atomic session updates.
+- Never reuse prekeys incorrectly.
+- Keep enough raw event data for debugging without leaking secrets.
+- Support schema migrations.
+- Support import/migration from current `signal-cli` data only if feasible and explicitly chosen.
+
+Suggested database areas:
+
+### Metadata
+
+Tables:
+
+- `schema_migrations`
+- `store_metadata`
+- `backend_metadata`
+
+Data:
+
+- Schema version.
+- Native backend version.
+- Upstream `libsignal` version.
+- Account identifier.
+- Device ID.
+- Created/updated timestamps.
+
+### Account And Device
+
+Tables:
+
+- `accounts`
+- `devices`
+- `account_credentials`
+- `service_tokens`
+
+Data:
+
+- ACI/PNI/service identifiers.
+- Local phone number if available.
+- Device ID.
+- Device name.
+- Registration state.
+- Service auth material.
+- Last successful sync.
+- Server capability flags.
+
+### Identity And Sessions
+
+Tables:
+
+- `identity_keys`
+- `recipient_identities`
+- `sessions`
+- `prekeys`
+- `signed_prekeys`
+- `pq_prekeys`
+- `kyber_prekeys`
+- `sender_keys`
+
+Data:
+
+- Local identity key pair.
+- Remote identity keys and trust decisions.
+- Per-recipient/per-device sessions.
+- One-time prekeys and upload state.
+- Signed prekeys and replacement schedule.
+- Post-quantum prekey material if required by current `libsignal`.
+- Sender key records for group messaging.
+
+Session writes must be transactional. A decrypt/encrypt operation that mutates session state must commit exactly once with the associated message state.
+
+### Recipients, Contacts, Profiles
+
+Tables:
+
+- `recipients`
+- `contacts`
+- `profiles`
+- `profile_keys`
+- `recipient_devices`
+- `recipient_capabilities`
+
+Data:
+
+- UUID/ACI/PNI mappings.
+- Phone numbers when available.
+- Display names.
+- Profile keys.
+- Avatar pointers.
+- Device lists.
+- Capability flags.
+- Blocked/muted/archived local state if needed.
+
+### Conversations And Messages
+
+Tables:
+
+- `conversations`
+- `messages`
+- `message_revisions`
+- `message_attachments`
+- `message_receipts`
+- `message_failures`
+- `outbox`
+- `pending_retries`
+
+Data:
+
+- Direct and group conversation identity.
+- Message body, timestamps, sender, recipient.
+- Delivery/read receipts.
+- Send attempts and per-recipient results.
+- Captcha/rate-limit failure records.
+- Retry scheduling.
+- Decrypted event linkage.
+
+Message plaintext storage policy must be decided explicitly. If plaintext is stored, document file permissions, backup behavior, and passphrase/encryption strategy.
+
+### Groups
+
+Tables:
+
+- `groups`
+- `group_members`
+- `group_roles`
+- `group_revisions`
+- `group_send_endorsements`
+- `group_pending_members`
+- `group_banned_members`
+
+Data:
+
+- Groups v2 identifiers.
+- Master keys or derived group secrets, if required.
+- Revision numbers.
+- Membership.
+- Roles.
+- Pending invite state.
+- Sender key state.
+- Access control metadata.
+
+### Attachments
+
+Tables:
+
+- `attachments`
+- `attachment_uploads`
+- `attachment_downloads`
+- `attachment_cache`
+
+Data:
+
+- CDN IDs.
+- Attachment pointers.
+- Encryption keys.
+- Digest/checksum.
+- MIME type.
+- Local file path.
+- Upload/download status.
+- Expiration/cleanup policy.
+
+Attachment plaintext should not be logged. Temporary files should be cleaned after failure.
+
+## 9. Network And Service Layer
+
+The native backend must implement the Signal service layer currently hidden by `signal-cli`.
+
+Major service areas:
+
+- Linked-device provisioning.
+- Authenticated HTTP requests.
+- WebSocket receive queue.
+- Message send endpoint.
+- Attachment upload endpoint.
+- Attachment download endpoint.
+- Profile fetch/update.
+- Contact discovery or recipient resolution.
+- Group state fetch/update.
+- Captcha/proof challenge submission.
+- Sync messages.
+- Receipts.
+- Prekey upload/refill.
+
+Implementation approach:
+
+1. Inventory current API behavior from `signal-cli` and `Signal-Android`.
+2. Define typed Jerboa request/response records.
+3. Generate or port protobuf message definitions where required.
+4. Implement one HTTP/WebSocket transport module with tracing hooks.
+5. Keep redaction centralized.
+6. Store enough request metadata to debug failures without storing secrets.
+
+Do not scatter endpoint URLs, auth headers, retry behavior, or redaction logic across feature modules.
+
+## 10. Receive Flow
+
+Target receive flow:
+
+```text
+connect receive websocket
+  -> fetch encrypted envelopes
+  -> persist raw envelope metadata
+  -> decrypt envelope with libsignal
+  -> classify message type
+  -> apply session mutations atomically
+  -> apply message/contact/group mutations atomically
+  -> acknowledge delivery to service
+  -> emit normalized app event
+  -> update history/TUI
+```
+
+Required message classes:
+
+- Plain direct messages.
+- Sealed sender direct messages.
+- Sync messages from linked devices.
+- Delivery receipts.
+- Read receipts.
+- Typing messages.
+- Contact/profile updates.
+- Attachment messages.
+- Group messages.
+- Group state updates.
+- Sender key distribution messages.
+- Session repair or retry messages.
+
+Initial MVP should support only what is needed for direct text messages, but the event model should not block the later classes.
+
+Receive exit criteria:
+
+- Can connect as a linked device.
+- Can receive a direct text message from a known contact.
+- Can decrypt the message.
+- Can store it in `jsqlite`.
+- Can show it in the existing history/TUI.
+- Can acknowledge it so it does not loop forever.
+- Can preserve and inspect a redacted trace when decryption fails.
+
+## 11. Send Flow
+
+Target direct send flow:
+
+```text
+user sends message
+  -> create outbox row
+  -> resolve recipient and devices
+  -> ensure sessions exist for each device
+  -> build content payload
+  -> encrypt per recipient/device with libsignal
+  -> submit service request
+  -> parse per-recipient/per-device result
+  -> persist final status or retry state
+  -> emit normalized send result
+```
+
+The send result must preserve:
+
+- Global request failure.
+- Captcha challenge.
+- Server rate limit with wait duration.
+- Per-recipient success/failure.
+- Per-device success/failure.
+- Stale device list failures.
+- Identity key change failures.
+- Network timeout.
+- Retryable vs permanent failure.
+
+This project already learned that "the command returned success" is not enough. The native backend must make success mean "the service accepted the message for the intended recipient/device set" or clearly label the narrower result.
+
+Direct send exit criteria:
+
+- Can send a direct text message to one recipient.
+- Can send to a recipient with multiple devices.
+- Can correctly handle missing/stale sessions.
+- Can report identity change separately from rate limit.
+- Can report captcha challenge without losing the composed message.
+- Can retry after challenge submission.
+- Can verify delivery using a second client during manual test.
+
+## 12. Captcha, Proofs, And Rate Limits
+
+Captcha and proof challenges are service-level controls. They cannot be treated as local throttles.
+
+Required behavior:
+
+- Preserve the original failed send request.
+- Preserve the challenge token or challenge metadata.
+- Display the exact challenge URL/token required by the service.
+- Submit the solved challenge through the same account/backend context.
+- Retry only when the service confirms challenge acceptance.
+- Keep the user-visible distinction between:
+  - local throttle,
+  - server rate limit,
+  - captcha required,
+  - challenge accepted but message not yet sent,
+  - retry succeeded,
+  - retry failed.
+
+Important operational caveat:
+
+- If a challenge is solved in a browser on a different network path than the client uses to submit/send, Signal may still reject or rate-limit the send. The implementation cannot guarantee success if the remote server and browser IP/reputation differ.
+
+Do not automatically spam retries after a challenge. A bad retry loop can worsen server-side reputation.
+
+## 13. Provisioning And Account Bootstrap
+
+The native backend should initially support linked-device mode, matching the current practical usage.
+
+Provisioning tasks:
+
+- Generate local identity/device material.
+- Request provisioning URI or QR payload.
+- Present provisioning URI/QR in CLI/TUI.
+- Accept provisioning response from Signal service.
+- Store account/device credentials in `jsqlite`.
+- Fetch initial contact/device/profile/group state.
+- Upload prekeys as needed.
+- Start receive queue.
+
+Exit criteria:
+
+- A fresh native store can link as a secondary device.
+- The official Signal client shows the linked device.
+- Native backend can receive a sync message.
+- Restarting the app preserves device state.
+- Unlinking/revocation is detected and presented clearly.
+
+Primary account registration is out of scope for the first port unless explicitly added. Linked-device support is enough to replace the current app usage and avoids more account-abuse-sensitive registration flows.
+
+## 14. Attachments
+
+Attachment support should come after direct text send/receive is reliable.
+
+Required behavior:
+
+- Upload attachment plaintext only after encrypting locally.
+- Store local pointer, digest, key, content type, and size.
+- Submit attachment pointer in message content.
+- Download encrypted attachment from CDN.
+- Verify digest/checksum.
+- Decrypt locally.
+- Store or stream plaintext according to local policy.
+- Clean temporary files on failure.
+
+Exit criteria:
+
+- Send one small image/file to a direct recipient.
+- Receive one small image/file from a direct recipient.
+- Reject corrupted downloads.
+- Show useful failure when upload succeeds but message send fails.
+- Avoid logging plaintext paths when privacy mode requires redaction.
+
+## 15. Groups V2
+
+Groups are the largest feature area after basic direct messaging.
+
+Required behavior:
+
+- Parse and store group identifiers and revisions.
+- Fetch group state.
+- Maintain member list and roles.
+- Process group update messages.
+- Maintain sender keys.
+- Send group messages.
+- Handle membership changes.
+- Handle profile/name/avatar metadata.
+- Respect access control and role rules.
+
+Likely upstream areas involved:
+
+- `libsignal` group/sender-key APIs.
+- `libsignal` zkgroup APIs.
+- Signal service group endpoints.
+- Signal-Android group update behavior.
+- `signal-cli` group send/fetch implementation.
+
+Group exit criteria:
+
+- Can receive group text message.
+- Can send group text message.
+- Can handle a member added after local state exists.
+- Can handle a member removed.
+- Can handle group revision changes.
+- Can recover from sender key missing/stale state.
+
+Groups should not be attempted until direct messaging and session persistence are boring.
+
+## 16. Sync Messages
+
+As a linked device, native backend must handle sync messages from the primary device and other linked devices.
+
+Required sync categories:
+
+- Sent messages from another linked device.
+- Read receipts.
+- Contact updates.
+- Profile updates.
+- Block list updates.
+- Sticker/attachment metadata if relevant.
+- Group state updates.
+- Configuration changes.
+
+Initial MVP can store unknown sync messages in a redacted raw event log and ignore them, but direct sent-message sync should be handled early so local history does not diverge badly.
+
+Exit criteria:
+
+- Sending from the official client appears in native history.
+- Reading in native client can emit or store read state consistently.
+- Unknown sync messages do not crash receive loop.
+
+## 17. Error Taxonomy
+
+Define one native error model and map every backend to it.
+
+Suggested categories:
+
+- `ok`
+- `network-timeout`
+- `network-unreachable`
+- `service-unavailable`
+- `unauthorized`
+- `device-unlinked`
+- `captcha-required`
+- `rate-limited`
+- `identity-changed`
+- `stale-device-list`
+- `session-missing`
+- `session-corrupt`
+- `decrypt-failed`
+- `attachment-upload-failed`
+- `attachment-download-failed`
+- `storage-failed`
+- `ffi-failed`
+- `protocol-unsupported`
+- `unknown`
+
+Every error should include:
+
+- Stable category.
+- Human-readable message.
+- Retryability.
+- Optional wait duration.
+- Optional challenge payload.
+- Optional recipient/device scope.
+- Redacted raw service code.
+- Trace ID.
+
+The UI should never parse raw upstream JSON directly.
+
+## 18. Tracing And Debugging
+
+Tracing is mandatory for this project because Signal service behavior changes and some failures are server-driven.
+
+Trace requirements:
+
+- Global trace ID per send/receive operation.
+- Redacted HTTP method/path/status.
+- Redacted WebSocket envelope metadata.
+- Backend version and upstream `libsignal` version.
+- Recipient count and device count, but not plaintext addresses unless debug mode explicitly allows it.
+- Send result classification.
+- Captcha/rate-limit state transitions.
+- Storage transaction boundaries for send/receive.
+- FFI call name, duration, and status, but not inputs containing secrets or plaintext.
+
+Recommended trace files:
+
+```text
+trace/
+  yyyy-mm-dd/
+    send-<trace-id>.jsonl
+    receive-<trace-id>.jsonl
+```
+
+Redaction should be centralized in one module. Do not rely on each caller remembering to redact.
+
+## 19. Testing Strategy
+
+Testing must be layered because the riskiest behavior spans FFI, storage, network, and external service state.
+
+### Unit Tests
+
+Rust:
+
+- Buffer allocation/free.
+- Error conversion.
+- Panic handling at FFI boundary.
+- JSON/protobuf input validation.
+- `libsignal` wrapper functions with local test vectors.
+
+Jerboa:
+
+- Backend interface conformance.
+- Send result normalization.
+- Error mapping.
+- Store migrations.
+- Store transaction behavior.
+- Redaction.
+- Retry scheduling.
+
+### Golden Fixtures
+
+Keep redacted fixtures for:
+
+- Successful direct send.
+- Captcha-required send.
+- Rate-limited send.
+- Per-recipient partial failure.
+- Successful direct receive.
+- Decrypt failure.
+- Attachment pointer message.
+- Group message.
+- Unknown future message type.
+
+Fixtures should be generated from:
+
+- Existing `signal-cli` traces.
+- Native backend traces.
+- Synthetic unit-level payloads.
+
+### Integration Tests
+
+Use at least two test Signal accounts/devices:
+
+- Account A official app plus native linked device.
+- Account B official app.
+- Optional Account C for group tests.
+
+Direct messaging matrix:
+
+- B sends to A native.
+- A native sends to B.
+- A official sends to B and native sync sees it.
+- B has multiple devices.
+- Device list changes between session setup and send.
+- Identity key change is detected.
+
+Attachment matrix:
+
+- Small text file.
+- Small image.
+- Larger file near service limit.
+- Corrupted download.
+- Failed upload retry.
+
+Group matrix:
+
+- Receive group message.
+- Send group message.
+- Add member.
+- Remove member.
+- Rename group.
+- Change avatar.
+- Sender key refresh.
+
+### Manual Tests
+
+Some flows cannot be reliably automated without risking service reputation:
+
+- Captcha challenge.
+- Long server rate-limit waits.
+- Account unlink/relink.
+- Suspicious-network behavior.
+
+Document exact manual commands and expected results for these flows.
+
+## 20. Security Requirements
+
+Security rules:
+
+- Let upstream `libsignal` handle cryptographic primitives.
+- Never implement custom crypto except glue required by upstream APIs.
+- Zeroize Rust buffers containing secrets when practical.
+- Do not log plaintext, private keys, session records, access keys, auth tokens, or attachment keys.
+- Mark secret-containing structs clearly.
+- Keep DB file permissions restrictive.
+- Keep temporary attachment files restrictive.
+- Avoid crash dumps containing secret memory.
+- Do not expose secret material through formatted exceptions.
+- Do not send secret payloads through generic debug printers.
+
+FFI-specific rules:
+
+- Validate all pointers and lengths.
+- Bound input sizes.
+- Catch panics.
+- Free all returned buffers exactly once.
+- Provide leak tests.
+- Prefer immutable byte slices at the boundary.
+- Do not hold Jerboa-owned pointers after a call returns.
+
+Storage-specific rules:
+
+- Key/session updates must be atomic with message processing.
+- Never reuse one-time prekeys after commit.
+- Treat failed transaction rollback carefully around decrypt/send operations.
+- Include migrations in tests.
+
+## 21. Build And Packaging
+
+Build requirements:
+
+- Build Rust crate before Jerboa final link.
+- Pin Rust toolchain or use a checked minimum supported Rust version.
+- Pin `libsignal` dependency exactly.
+- Include upstream version in binary metadata.
+- Include all license files in release artifacts.
+- Support clean rebuild from source.
+
+Proposed Makefile targets:
+
+```make
+native-build
+native-test
+native-clean
+native-header
+native-check
+test-native-backend
+```
+
+Proposed generated artifacts:
+
+```text
+native/signal-native/target/
+native/signal-native/include/signal_native.h
+build/libsignal_native.a
+build/signal-native-build-info.json
+```
+
+Do not commit Rust `target/` output.
+
+## 22. Version Tracking
+
+Every release should record:
+
+- `jerboa-signal` git commit.
+- `jsqlite` git commit.
+- `libsignal` version and git commit.
+- Signal service behavior reference:
+  - `Signal-Android` commit inspected.
+  - `signal-cli` commit or release inspected.
+- Protobuf/schema generation timestamp.
+- Test account matrix last passed date.
+
+Weekly upstream watch list:
+
+- `libsignal` releases.
+- `Signal-Android` service/protobuf changes.
+- `signal-cli` release notes and bug fixes.
+- Server-side breakage reports from issue trackers.
+
+Version bump checklist:
+
+1. Read upstream release notes.
+2. Diff public `libsignal` APIs used by `signal-native`.
+3. Diff service model/protobuf changes in `Signal-Android`.
+4. Diff `signal-cli` send/receive/provisioning behavior.
+5. Update pinned dependency.
+6. Regenerate protobuf bindings if needed.
+7. Run Rust unit tests.
+8. Run Jerboa unit tests.
+9. Run direct send/receive integration tests.
+10. Run attachment tests if affected.
+11. Run group tests if affected.
+12. Update build metadata.
+
+## 23. Phased Roadmap
+
+The estimates below assume one experienced engineer with access to test accounts and enough time to chase upstream behavior. A less experienced model or engineer should treat these as lower bounds.
+
+### Phase 0: Inventory And Guardrails
+
+Estimate: 1 to 2 weeks.
+
+Tasks:
+
+- Pin the current known-good `signal-cli` release used as a behavioral reference.
+- Pin the current `libsignal` release.
+- Clone or vendor references for `Signal-Android`, `signal-cli`, and `libsignal`.