Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion docs/quality-plans/conversation-persistence-tiering-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
14 changes: 10 additions & 4 deletions docs/quality-plans/conversation-persistence-tiering-tech-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
Expand All @@ -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.

Expand Down Expand Up @@ -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,
Expand Down
Loading