diff --git a/docs/quality-plans/conversation-persistence-tiering-plan.md b/docs/quality-plans/conversation-persistence-tiering-plan.md index aea2ca5..feccf77 100644 --- a/docs/quality-plans/conversation-persistence-tiering-plan.md +++ b/docs/quality-plans/conversation-persistence-tiering-plan.md @@ -103,6 +103,41 @@ incremental no hot path está custando latência percebida, o que hoje **não** caso (`docs/cost-latency-profile.md`: orquestração soma single-digit de ms; quase toda a latência é o round-trip do modelo). +### Decisão fechada (2026-06-29): **A**, com escada incremental para as ideias da B + +Avaliamos um desenho alternativo (Renan): RAM 45m → Redis 7d como **verdade +operacional** → snapshot off-box → Postgres só de madrugada. É a Opção B com um hot +tier na frente. Onde a B/Renan **falha no nosso cenário** e a A não: + +- **Backup por snapshot periódico do Redis** perde tudo desde o último snapshot se a + VPS inteira morrer (AOF não salva se a máquina sumiu). A A faz **append por-turno + off-box via outbox** (perda ≈ segundos). +- **Madrugada como fronteira de durabilidade** já foi rejeitada (ver §3); a B reintroduz + isso para os turnos não-resolvidos do dia. +- **Carga operacional**: a B transforma o Redis em dado crítico (AOF + snapshot + + eviction segura + restore + reconciliação noturna) — superfície demais para um time + de dois. A A reaproveita write-through + outbox + sink que **já existem**. + +Onde a B é genuinamente melhor (e por isso vira *escalada futura*, não descarte): +aceitar writes com **Postgres fora** e **vazão em concorrência altíssima** — nenhum +dos dois é a restrição de hoje (milhares/**dia**, não milhares **simultâneos**). + +**Regra de ouro (vale em toda escala): o que é síncrono é a âncora de durabilidade; +o backup off-box é sempre append por-turno — nunca snapshot/flush diário como +fronteira de durabilidade.** + +Escada de escalada (só sobe quando o gatilho aparecer, sem virar a B inteira): + +| Nível | Gatilho que justifica | O que liga (flag) | Vem da ideia | +| --- | --- | --- | --- | +| **0 (agora)** | — | Postgres write-through (âncora) + outbox (R2 por-turno + fan-out) + estado quente in-memory fail-open + resumo noturno | A | +| **1** | restart/multi-worker perde estado de sessão | `RedisSessionStateStore` (estado quente em Redis, TTL, **não-autoritativo**, fail-open) | "RAM 45m / Redis 7d" da B | +| **2** | leitura de histórico/triagem de tickets-em-aberto vira gargalo | cache de leitura 7d no Redis por cima do Postgres | "meio campo" operacional da B | +| **3** | write-through vira gargalo **ou** requisito de aceitar com Postgres fora | promover **localmente** (só no domínio que provar) o caminho para Redis-first (Opção B) | âncora-Redis da B, localizada e por evidência | + +Assim a A **cresce para dentro da B** de forma incremental e reversível, em vez de +pagar a complexidade adiantado por uma carga que ainda não existe. + --- ## 5. Tradeoffs honestos / limites de MVP @@ -232,7 +267,8 @@ backend (in-memory, Redis, S3) trocável por flag, sem mudar chamador nem schema ## 11. Decisões em aberto -- Opção **A vs B** da §4 (recomendado A). Confirmar antes da fatia 4. +- ~~Opção **A vs B** da §4~~ **RESOLVIDA (2026-06-29): A**, com escada de escalada + incremental para absorver as ideias da B sob gatilho (ver §4, "Decisão fechada"). - Retenção exata de cada camada (45 min / 7 d são pontos de partida, não lei). - Política de `maxmemory`/eviction do Redis que **não** descarte turno não sumarizado. diff --git a/docs/quality-plans/conversation-persistence-tiering-tech-plan.md b/docs/quality-plans/conversation-persistence-tiering-tech-plan.md index cff4e67..b6e3128 100644 --- a/docs/quality-plans/conversation-persistence-tiering-tech-plan.md +++ b/docs/quality-plans/conversation-persistence-tiering-tech-plan.md @@ -11,8 +11,9 @@ Princípios herdados do projeto (não negociar): - **Nunca** persistir `session_id` cru nem PII livre; reusar `hash_session`, `sanitize_payload`, `redaction_version`. - Nada novo no **hot path** do `/chat` sem flag desligável e com *fail-open*. -- Migrations **forward-only** com ledger (`python -m scripts.migrate`); próxima é a - `010_`. +- Migrations **forward-only** com ledger (`python -m scripts.migrate`). A `011` já + está aplicada (em prod inclusive); `010` é um gap de numeração não usado. Para não + aplicar fora de ordem, a próxima migration nova é a **`012_`**. - `python -m pytest` + `python -m compileall app tests scripts` em toda fatia. Recomendação de durabilidade desta frente: **opção A** do plano de decisão — @@ -23,6 +24,11 @@ turnos; Redis é cache/estado por cima. As fases abaixo respeitam isso. ## Fase 0 — Operacionalizar o sink off-box (sem código novo) +Status (2026-06-29): **prep feita, bloqueada só por credenciais R2.** Na VPS o +`boto3` já está no `.venv` e o worker systemd `supportfaq-outbox.service` existe +dormente (disabled/inactive); a outbox está limpa. Falta só o destino R2 +(bucket + endpoint + Access Key/Secret) para ligar as flags abaixo e o worker. + Fecha o gap de perda **antes** de qualquer Redis. Já documentado em `docs/conversation-archive-sink.md`; aqui só o checklist de execução. @@ -165,10 +171,10 @@ def build_session_state_store_from_env() -> SessionStateStore: Postgres como base analítica/RAG, alimentada por batch idempotente às ~3h. ### Migration -- **Novo** `migrations/010_conversation_summaries.sql` (forward-only, ledger): +- **Novo** `migrations/012_conversation_summaries.sql` (forward-only, ledger): ```sql CREATE TABLE conversation_summaries ( - id BIGGENERATED ... PRIMARY KEY, + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, domain TEXT NOT NULL, customer_ref TEXT NOT NULL, -- id/hash estável, NUNCA cru problem TEXT NOT NULL,