## Contract Note 004: Message Envelope & Translation Contract

**State:** proposed → responded
**Contract Version:** (unlocked)

**Current Shape:**

None—the ADR specifies backend translates via external service but doesn't name the contract.

**Proposed Change:**

Message envelope shape, translation timing, error handling.

**Backend sends message object with:**
- `id` (UUID, generated by backend)
- `chat_id` (UUID, references the conversation)
- `sender_id` (UUID, references the user)
- `original_text` (string, user's input language)
- `original_language` (enum: EN, DE, JA; inferred from sender preference)
- `translated_text` (string or null; populated after translation service returns)
- `translation_status` (enum: pending, translated, failed; defaults to pending)
- `translation_error` (string or null; populated if service fails)
- `created_at` (ISO 8601 timestamp)

**Translation timing:**
Backend translates synchronously during message insertion (within 3s timeout per ticket 003). If translation takes >1s, consider async—send message immediately with `translation_status=pending`, emit update event when translation completes. This is open pending your latency budget.

**Error handling:**
If translation service fails after 3 retries, `translation_status=failed` and `translation_error` is populated. Frontend renders original text with error indicator. Original message is persisted and sent regardless—translation failure does not block message send.

**Deletion semantics (GDPR 17):**
When user deletes a message, backend soft-deletes (sets `deleted_at` timestamp). Frontend does not render soft-deleted messages. If translation was cached externally, this is open—does deletion trigger a purge request to the translation service, or do we accept that the processor may retain plaintext? (Escalate to Queen if GDPR interpretation is needed.)

**Source:** ADR tradeoff: plaintext translation means processor sees message before translation; deletion compliance is open. Ticket 003 names translation latency as uncertain. Ticket 004 needs send/receive SLO.

**Frontend Impact (Tweedledee):**

I can work with this envelope. Translation timing is the load-bearing detail: **I'm asserting async translation is required for <2s SLO.** Here's why: if the translation service is external (DeepL), even at p50 latency ~500ms, at p95 it's easily 1.5–2s. Adding that to message insert + network RTT pushes us past <2s target. 

My handling: **async model**. Frontend sends message, receives original immediately with `translation_status='pending'`. I render the original text in the message list right away (pending state: slightly muted/italic, no error). When translation arrives (either in the same WebSocket event as send response, or in a follow-up update event per contract 006), I update the message in-place to `translation_status='translated'` + new `translated_text`. If translation fails, I set `translation_status='failed'` and show a small error badge; original remains readable.

**Client state:** I'll track `{ message_id, translation_status, translated_text, translation_error }` in the message object local state. The list is ordered by created_at; each message updates in-place when translation status changes. No local translation—I trust the backend service.

**Open question for backend:** When async translation completes, does the update event come via WebSocket (option A from contract 006) with the same `message` object (id + new translation_status/translated_text)? Or do I need to poll for updates? I'm assuming WebSocket per contract 006's proposal, and I need the update event to include at minimum `{ id, translation_status, translated_text }`.

**Deletion**: I'll expose a delete button for user's own messages. Delete action sends DELETE /messages/{message_id}. On success, I remove it from the list. On failure (e.g., already deleted), show error toast. The soft-delete behavior (deleted_at flag) is backend concern; I just stop rendering.

**GDPR**: Translation service cache + deletion is a **Queen concern**, not a contract-seam issue. Recommend contract note escalation to Queen if not already handled.

**Backend Impact (Tweedledum):**

Backend owns message persistence, translation call (with timeout + retry), status enum management, and soft-delete. Translating synchronously risks latency; async risks complexity (async queue, WebSocket update event). Current proposal: synchronous with 3s timeout; if timeout triggers, send message with `translation_status=pending` and schedule async translation. This requires a background worker or post-insert hook—deferred to fast-follow if too expensive for v1.

---

**Tweedledee's assessment:** I'm **marking this ready to agree** pending one clarification: on async translation complete, does the backend emit a WebSocket update event with the translated message? If yes, this contract is locked. If backend can't support WebSocket updates in v1, we need to escalate to the Cat because the <2s SLO becomes at risk.
