Blog

Janela de contexto compartilhada entre canais: WhatsApp, web e telefone

O cliente explicou o problema inteiro no WhatsApp na terça, mandou o comprovante pelo chat do site na quarta e ligou na quinta. Do lado dele isso é uma conversa só. Do lado do seu sistema são três conversas que nunca se encontraram, e por isso ele contou a mesma história três vezes e ainda ouviu que não havia registro do comprovante. A tentação é resolver por identidade: achar que é a mesma pessoa e concatenar tudo num histórico único. Isso quebra em dois lugares ao mesmo tempo, porque telefone não é identidade e porque a janela do modelo não cresce quando o cliente fala mais. Este artigo mostra como construir a janela compartilhada de verdade: por que o canal precisa continuar existindo como dimensão, como vincular identidades sem confundir pessoas, como montar a janela por orçamento e não por concatenação, como escrever concorrente entre canais sem perder mensagem e como reconhecer o caso em que juntar contexto é a decisão errada.

2026-08-02 / IA Aplicada / 14 min

01

Continuidade não é concatenação: o que o cliente espera de fato

A frase que descreve o problema é sempre a mesma na reunião: o atendimento precisa lembrar do que foi falado no outro canal. O que quase nunca é discutido é o quanto precisa lembrar, e essa é a decisão que define a arquitetura inteira. O cliente não espera que o atendente por telefone recite o que ele digitou no chat, ele espera não ter que repetir o motivo do contato, o número do pedido e o que já foi tentado. São coisas diferentes: a primeira é transcrição, a segunda é estado. Sistemas que tentam entregar transcrição entre canais gastam contexto caro com histórico literal e ainda assim erram, porque a informação que importava estava diluída em quarenta mensagens de ida e volta.

A separação prática é entre três camadas com ciclos de vida diferentes. O fato durável é o que continua verdadeiro independentemente do canal e do tempo: o pedido em disputa, o endereço confirmado, a preferência de contato, a decisão já tomada pelo suporte. O resumo do episódio é o que aconteceu naquele atendimento específico, condensado em poucas frases assim que ele termina. E a transcrição bruta é o turno a turno, que serve para auditoria e para o operador humano ler, mas quase nunca precisa entrar no prompt de outro canal. Quem trata as três como se fossem a mesma coisa acaba enfiando transcrição em tudo, e é assim que uma conversa de dez minutos no telefone vira um prompt que não cabe na janela e ainda custa três vezes mais.

CamadaCiclo de vidaEntra no prompt de outro canalCusto típico
Fato durávelMeses, até ser contraditoSempre, é o núcleo da continuidadeDezenas de tokens, cabe inteiro
Resumo do episódioSemanas, decai por relevânciaOs dois ou três mais recentesCentenas de tokens por episódio
Transcrição brutaRetenção legal, meses ou anosRaramente, só o episódio em cursoMilhares de tokens, estoura rápido
Estado operacionalVida do ticketSempre, define o que pode ser feitoDezenas de tokens, é estruturado

Essa tabela também responde por que a resposta ingênua falha em produção sem dar erro. Concatenar canais funciona no piloto porque o cliente de teste tem três mensagens em cada um. Com um cliente real de seis meses, o histórico cruzado passa de qualquer janela, e a estratégia de cortar os mais antigos apaga justamente o fato durável, que costuma ter sido dito no primeiro contato. O sistema fica pior que o desconectado: além de não lembrar, ele lembra da coisa errada com confiança.

02

Vincular identidades sem juntar duas pessoas na mesma conversa

Antes de compartilhar qualquer contexto é preciso decidir de quem ele é, e essa é a parte onde os erros são caros de forma assimétrica. Deixar de vincular o mesmo cliente entre canais custa uma repetição irritante. Vincular pessoas diferentes ao mesmo perfil vaza dado de uma para a outra, e no Brasil o número de telefone reciclado pela operadora torna isso comum o bastante para não ser um caso de borda. O celular que hoje é do seu cliente pode ter sido de outra pessoa há oito meses, e o WhatsApp Business vai entregar essa mensagem no mesmo identificador que você usou como chave.

O desenho que resolve isso é separar o identificador do canal da identidade da pessoa, com um nível de confiança em cada vínculo. O identificador do canal é o número de WhatsApp, o identificador anônimo do widget web, o número de origem da ligação. A identidade é o cliente no seu domínio. Entre os dois existe um vínculo com origem e força: vínculo confirmado por autenticação vale para tudo, vínculo inferido por coincidência de número vale para retomar o assunto do dia, e nunca para expor dado cadastral ou financeiro. A regra que evita o vazamento é simples de enunciar e precisa ser explícita no código: o que o vínculo fraco libera é continuidade de assunto, não acesso a dado.

// identity-link.js
// Vinculo entre identificador de canal e identidade do cliente, com nivel
// de confianca. O nivel decide o que pode ser exposto, nao apenas se as
// conversas se juntam: numero reciclado pela operadora e caso comum.

export const LINK_STRENGTH = {
  AUTHENTICATED: 'authenticated', // login, OTP ou token de sessao valido
  DECLARED: 'declared',           // cliente informou pedido/CPF e conferiu
  INFERRED: 'inferred',           // mesmo numero, sem confirmacao ativa
};

const RANK = { inferred: 1, declared: 2, authenticated: 3 };

// Vinculo inferido expira rapido: apos esse prazo o mesmo numero volta a
// ser tratado como desconhecido ate nova confirmacao.
const INFERRED_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;

export const resolveIdentity = ({ channel, channelId, links, now }) => {
  const candidates = links
    .filter((link) => link.channel === channel && link.channelId === channelId)
    .filter((link) => {
      if (link.strength !== LINK_STRENGTH.INFERRED) return true;
      return now - link.confirmedAt <= INFERRED_MAX_AGE_MS;
    })
    .sort((a, b) => RANK[b.strength] - RANK[a.strength] || b.confirmedAt - a.confirmedAt);

  // Dois clientes distintos no mesmo identificador sem vinculo forte:
  // numero provavelmente trocou de dono. Nao adivinhe, pergunte.
  const distinct = new Set(candidates.map((link) => link.customerId));
  if (distinct.size > 1 && candidates[0].strength !== LINK_STRENGTH.AUTHENTICATED) {
    return { customerId: null, strength: null, needsDisambiguation: true };
  }

  const best = candidates[0];
  if (!best) return { customerId: null, strength: null, needsDisambiguation: false };
  return { customerId: best.customerId, strength: best.strength, needsDisambiguation: false };
};

// O que cada nivel libera. Vinculo fraco da continuidade de assunto,
// nunca dado cadastral, financeiro ou acao com efeito colateral.
const SCOPE_BY_STRENGTH = {
  inferred: ['episode_summary', 'open_ticket_subject'],
  declared: ['episode_summary', 'open_ticket_subject', 'order_status', 'durable_facts'],
  authenticated: [
    'episode_summary',
    'open_ticket_subject',
    'order_status',
    'durable_facts',
    'billing',
    'personal_data',
    'mutating_tools',
  ],
};

export const allowedScopes = (strength) => SCOPE_BY_STRENGTH[strength] ?? [];

export const canExpose = (strength, scope) => allowedScopes(strength).includes(scope);

A checagem de identificador com mais de um cliente merece atenção porque é ela que transforma o vazamento em pergunta. Quando o mesmo número aparece ligado a duas identidades sem vínculo forte, o sistema não deve escolher a mais recente: deve tratar como desconhecido e pedir confirmação de um dado que só o titular sabe. Custa uma pergunta a mais no começo da conversa e evita o incidente que ninguém quer reportar. A expiração do vínculo inferido tem a mesma função no eixo do tempo, porque o número que ficou seis meses parado é justamente o que tem maior chance de ter mudado de dono.

03

Montar a janela por orçamento, não por concatenação

Resolvida a identidade, a pergunta seguinte é o que vai no prompt. A resposta errada é tudo que couber, começando pelo mais recente, porque isso deixa a composição do contexto na mão do acaso: um cliente que mandou vinte áudios transcritos ontem apaga o fato relevante de três meses atrás. A composição precisa ser um orçamento com fatias reservadas por tipo de informação, e cada fatia é preenchida pela sua própria regra de prioridade. Assim, o pior caso deixa de ser a perda do que importa e passa a ser a perda de detalhe dentro de uma categoria.

Orcamento de janela em atendimento multicanal (exemplo com 8k tokens)

  instrucao do sistema        800  fixo, nao negociavel
  estado operacional          400  ticket aberto, canal, permissoes
  fatos duraveis            1.200  ordenados por relevancia ao assunto atual
  resumo de episodios       1.600  ultimos episodios, qualquer canal
  episodio atual            3.200  turnos recentes deste canal, integral
  reserva de saida            800  nao usar, e o teto da resposta

Regra de preenchimento
  cada fatia so cede o que sobrou para a fatia seguinte, nunca puxa da anterior
  fato duravel nunca e cortado por idade, so por irrelevancia ao assunto
  episodio de outro canal entra resumido, nunca em transcricao
  se o episodio atual nao cabe, comprime o meio e preserva inicio e fim

A regra de que uma fatia cede o excedente para a seguinte, mas nunca invade a anterior, é o que impede o efeito mais comum em produção: o histórico recente engolindo o espaço dos fatos. Ela também dá uma propriedade útil de operação, que é a previsibilidade do custo. Com fatias fixas, o token gasto por turno tem teto conhecido independentemente de quanto o cliente falou, e a fatura para de ser função do cliente mais falante. Sem isso, um único usuário com uma conversa de duzentos turnos define o custo médio da sua operação inteira.

// context-window.js
// Montagem da janela por orcamento com fatias por tipo. Cada fatia gasta
// o que precisa e passa a sobra adiante; nenhuma invade a anterior.

const BUDGET = {
  systemInstruction: 800,
  operationalState: 400,
  durableFacts: 1200,
  episodeSummaries: 1600,
  currentEpisode: 3200,
};

// Aproximacao suficiente para orcamento. Use o tokenizer do provedor
// quando a margem importar mais que a latencia da contagem.
const estimateTokens = (text) => Math.ceil(text.length / 3.6);

const fill = (items, budget) => {
  const selected = [];
  let used = 0;
  for (const item of items) {
    const cost = estimateTokens(item.text);
    if (used + cost > budget) continue; // pula o que nao cabe, segue tentando os menores
    selected.push(item);
    used += cost;
  }
  return { selected, used };
};

export const buildWindow = ({ channel, subject, state, facts, summaries, episode, scopes }) => {
  let spare = 0;

  // Fatos duraveis: ordenados por relevancia ao assunto atual, filtrados
  // pelo escopo que o nivel de vinculo autoriza a expor.
  const visibleFacts = facts
    .filter((fact) => scopes.includes(fact.scope))
    .sort((a, b) => relevance(b, subject) - relevance(a, subject));
  const factsFill = fill(visibleFacts, BUDGET.durableFacts);
  spare += BUDGET.durableFacts - factsFill.used;

  // Resumos de episodios: mais recentes primeiro, de qualquer canal.
  // O canal de origem entra no texto porque muda como a informacao e lida.
  const summaryItems = summaries
    .slice()
    .sort((a, b) => b.endedAt - a.endedAt)
    .map((item) => ({ ...item, text: `[${item.channel}] ${item.text}` }));
  const summariesFill = fill(summaryItems, BUDGET.episodeSummaries + spare);
  spare = BUDGET.episodeSummaries + spare - summariesFill.used;

  // Episodio atual: preserva o inicio, onde o cliente disse o que quer,
  // e o fim, que e o estado da conversa. O meio e o que se comprime.
  const currentBudget = BUDGET.currentEpisode + spare;
  const current = compressMiddle(episode.turns, currentBudget, estimateTokens);

  return {
    channel,
    blocks: {
      state,
      facts: factsFill.selected,
      summaries: summariesFill.selected,
      turns: current.turns,
    },
    estimatedTokens:
      BUDGET.systemInstruction +
      BUDGET.operationalState +
      factsFill.used +
      summariesFill.used +
      current.used,
    droppedTurns: current.dropped,
  };
};

// Corta do meio para fora, mantendo os primeiros e os ultimos turnos.
const compressMiddle = (turns, budget, estimate) => {
  const costs = turns.map((turn) => estimate(turn.text));
  const total = costs.reduce((a, b) => a + b, 0);
  if (total <= budget) return { turns, used: total, dropped: 0 };

  const kept = [];
  let used = 0;
  let head = 0;
  let tail = turns.length - 1;

  // Alterna entre inicio e fim para nao enviesar o corte para um dos lados.
  while (head <= tail) {
    const takeTail = kept.length % 2 === 1;
    const index = takeTail ? tail : head;
    if (used + costs[index] > budget) break;
    kept.push({ index, turn: turns[index] });
    used += costs[index];
    if (takeTail) tail -= 1;
    else head += 1;
  }

  kept.sort((a, b) => a.index - b.index);
  return {
    turns: kept.map((item) => item.turn),
    used,
    dropped: turns.length - kept.length,
  };
};

const relevance = (fact, subject) => {
  const overlap = fact.tags.filter((tag) => subject.tags.includes(tag)).length;
  const ageDays = (Date.now() - fact.confirmedAt) / 86_400_000;
  return overlap * 10 - Math.log1p(ageDays);
};

O detalhe do canal de origem entrar no texto do resumo não é cosmético. O modelo precisa saber que aquele trecho veio do telefone para tratar a transcrição com a incerteza que ela merece, e precisa saber que veio do chat web para confiar no número de pedido que foi digitado. Sem essa marca, a transcrição imprecisa de um áudio ruim entra no prompt com o mesmo peso de um dado que o cliente conferiu na tela, e o modelo repete o erro com confiança total.

04

Escrita concorrente entre canais: o cliente pode estar em dois ao mesmo tempo

O caso que quebra a implementação simples não é o cliente que troca de canal, é o que usa dois ao mesmo tempo. Ele está na ligação com o atendente e enquanto isso manda a foto do produto pelo WhatsApp, ou pergunta no chat web enquanto o bot ainda está processando a mensagem anterior do WhatsApp. Se o estado compartilhado é lido, modificado e escrito por cada canal sem coordenação, a última escrita apaga a outra em silêncio: o fato que o atendente acabou de registrar some porque o bot gravou por cima com uma versão do estado lida cinco segundos antes.

A solução que se paga é modelar o estado compartilhado como fatos apensados com versão, e não como um documento sobrescrito. Cada canal escreve um fato novo com o carimbo de origem e de tempo, e a leitura resolve o conflito na hora de montar a janela, aplicando a regra de precedência do domínio. Isso evita o bloqueio distribuído no caminho quente e ainda deixa a divergência visível em vez de silenciosa. A ordenação por tempo do servidor resolve a maioria dos casos, e a exceção que merece regra explícita é a contradição entre canais de confiança diferente.

  • Fato confirmado por autenticação vence fato inferido, independentemente de qual chegou depois, porque um foi conferido e o outro foi deduzido.
  • Entre fatos de mesma força, o mais recente vence, usando o carimbo do servidor e não o do dispositivo, que pode estar com relógio errado ou fora de ordem por fila.
  • Contradição direta em campo sensível, como endereço de entrega ou dado de pagamento, não se resolve sozinha: marca o campo como em disputa e força confirmação no próximo turno.
  • Ação com efeito colateral, como emitir segunda via ou cancelar pedido, precisa de chave de idempotência que inclua a identidade e não o canal, senão a mesma ação disparada de dois canais executa duas vezes.
  • Episódio ainda aberto em outro canal entra na janela como estado, não como histórico, para que o modelo saiba que existe um atendimento humano em curso e não tente resolver por conta própria.

O último item é o que mais melhora a experiência percebida com menos código. Um bot que responde no WhatsApp enquanto o cliente está falando com um humano no telefone produz respostas conflitantes na mesma janela de dois minutos, e o cliente conclui, com razão, que a empresa não fala com ela mesma. Saber que existe um episódio ativo em outro canal permite a resposta certa, que é reconhecer o atendimento em curso e não competir com ele.

05

O canal continua importando depois de compartilhar o contexto

Compartilhar o contexto não significa apagar a diferença entre os canais, e o erro simétrico ao de não compartilhar nada é tratar tudo como se fosse a mesma superfície. Cada canal tem restrição própria de formato, de latência e de confiabilidade da entrada, e essas restrições precisam entrar na montagem do prompt junto com o histórico. A resposta correta no chat web, com lista de opções e um link, é uma resposta ruim quando lida em voz alta pelo telefone, e o mesmo texto de trezentas palavras que funciona na web vira quatro balões que o cliente não lê no WhatsApp.

DimensãoWhatsAppChat webTelefone
Confiabilidade da entradaAlta em texto, média em áudio transcritoAlta, o cliente confere o que digitouMédia, transcrição erra número e nome próprio
Formato da respostaCurto, sem markdown, quebra em poucos balõesEstruturado, aceita lista, tabela e linkFrase falada, sem enumeração longa nem URL
Tolerância de latênciaSegundos, conversa é assíncronaPoucos segundos, cliente está olhandoMenos de um segundo, silêncio é falha
SessãoNão existe fim explícito, some por inatividadeTermina quando fecha a abaTermina no desligar, sem despedida garantida
Confirmação de dado sensívelPode pedir digitação, texto fica registradoIdeal, permite formulário e mascaramentoEvitar, número falado em voz vaza no ambiente

A linha da confiabilidade da entrada é a que mais gera bug sutil. Quando um número de pedido chega pela transcrição de uma ligação, ele deve entrar no contexto marcado como não confirmado, e a primeira coisa a fazer é validar contra a base antes de tratá-lo como fato durável. Sem essa marca, o número transcrito errado vira fato, contamina os episódios seguintes em todos os canais e o cliente passa a ser atendido sobre um pedido que não é o dele. Corrigir isso depois é mais difícil do que parece, porque o fato errado já foi propagado como se fosse verdade estabelecida.

06

Quando não compartilhar é a decisão certa

Existe um conjunto de situações em que a continuidade entre canais é um defeito, e vale enunciá-las porque a pressão natural do projeto é compartilhar sempre mais. A primeira é o canal com nível de autenticação mais fraco: trazer para o WhatsApp inferido o assunto de um atendimento que foi autenticado no aplicativo expõe informação a quem tiver o aparelho na mão, e isso inclui o cenário mundano do celular emprestado. A segunda é o contexto que envelheceu: retomar automaticamente um assunto de quatro meses atrás porque o cliente escreveu oi é pior do que perguntar, e produz aquele atendimento que insiste em um problema que já foi resolvido por outro caminho.

  1. Canal de destino com vínculo mais fraco que o de origem: compartilha apenas o assunto em aberto, sem dado cadastral, financeiro ou histórico de reclamação.
  2. Episódio encerrado com resolução há mais de noventa dias: fica disponível para busca, mas não entra na janela por padrão, porque a probabilidade de o cliente estar voltando naquele assunto é baixa.
  3. Assunto marcado como sensível pelo domínio, como jurídico, saúde ou disputa de cobrança: exige confirmação explícita do cliente antes de aparecer em outro canal, mesmo com identidade autenticada.
  4. Atendimento em canal compartilhado por natureza, como um telefone fixo de empresa ou um WhatsApp de setor: o vínculo por identificador não pode ser inferido, porque a pessoa por trás muda a cada ligação.
  5. Cliente que pediu explicitamente para tratar o assunto de forma separada: a preferência é um fato durável e precisa ser respeitada pela montagem da janela, não apenas registrada no CRM.

O quarto item costuma passar despercebido até virar incidente. Um número de WhatsApp de um setor de uma empresa cliente recebe mensagens de pessoas diferentes ao longo do dia, e o vínculo inferido por identificador junta todas na mesma identidade. O sintoma aparece como um bot que confunde solicitações, e a causa é ter tratado um identificador compartilhado como se fosse pessoal. A saída é marcar esses identificadores explicitamente no cadastro e exigir identificação por turno neles, o que é uma fricção aceitável em um contexto onde ela já é esperada.

07

Verificar que a continuidade funciona antes do cliente testar

Continuidade entre canais é o tipo de funcionalidade que passa em todos os testes unitários e falha no primeiro uso real, porque o que quebra está nas junções e não nas partes. O teste que importa cruza canal, tempo e identidade ao mesmo tempo, e ele precisa ser automatizado, senão só será executado quando alguém lembrar. Vale montar um conjunto pequeno de cenários ponta a ponta que exercitem exatamente os pontos onde a arquitetura pode falhar em silêncio.

  1. Retomada simples: abre um episódio no WhatsApp com um fato durável, encerra, abre outro no chat web e confirme que o fato aparece na janela e a transcrição do primeiro não aparece.
  2. Concorrência entre canais: escreva um fato pelo canal A e outro contraditório pelo canal B na mesma janela de segundos, e verifique que nenhum dos dois sumiu e que a precedência aplicada foi a do domínio, não a da ordem de chegada.
  3. Número reciclado: registre dois clientes distintos no mesmo identificador com vínculo inferido e confirme que o sistema pede desambiguação em vez de escolher o mais recente.
  4. Estouro de orçamento: injete um episódio com centenas de turnos e verifique que os fatos duráveis continuam na janela, que o corte aconteceu no meio do episódio atual e que o total estimado respeitou o teto.
  5. Escopo por força de vínculo: com vínculo inferido, confirme que o assunto em aberto aparece e que nenhum campo de dado pessoal ou financeiro entrou no prompt, inspecionando o payload final e não a resposta.
  6. Episódio ativo em paralelo: com um atendimento humano em andamento no telefone, envie uma mensagem no WhatsApp e verifique que o bot reconhece o atendimento em curso em vez de responder por conta própria.

O quinto cenário merece ser escrito contra o payload que sai para o provedor, e não contra o texto que o cliente recebe. É perfeitamente possível que o modelo não mencione o dado pessoal na resposta e ele tenha ido no prompt do mesmo jeito, e nesse caso o vazamento já aconteceu: o dado saiu do seu perímetro, está no log de requisição e possivelmente no cache de prompt do provedor. O teste que olha só a resposta passa e a auditoria reprova. Verificar no payload transforma uma discussão de política em uma asserção que quebra o build.

FAQ

Perguntas frequentes

Dá para simplesmente concatenar o histórico dos canais em um prompt só?

Funciona no piloto e falha em produção, sem dar erro, que é o pior modo de falhar. O cliente de teste tem três mensagens em cada canal, e o cliente real de seis meses tem um histórico cruzado que estoura qualquer janela. Quando estoura, a estratégia usual de cortar o mais antigo apaga justamente o fato durável, porque ele quase sempre foi dito no primeiro contato, e o sistema fica pior do que estaria desconectado: não lembra do que importa e lembra do irrelevante com total confiança. O caminho que funciona é separar três camadas com ciclos de vida diferentes, que são o fato durável, o resumo do episódio e a transcrição bruta, e montar a janela com fatias de orçamento reservadas para cada uma. A transcrição de outro canal quase nunca precisa entrar, o resumo entra quase sempre e o fato durável nunca é cortado por idade, apenas por irrelevância ao assunto atual. Com fatias fixas o custo por turno também ganha teto conhecido, em vez de ser definido pelo cliente mais falante da base.

Como evitar juntar duas pessoas no mesmo contexto quando o número de telefone é reciclado?

Separando o identificador do canal da identidade da pessoa e anexando uma força a cada vínculo. O número de WhatsApp, o identificador do widget web e o número de origem da ligação são identificadores de canal; o cliente é a identidade. O vínculo entre eles tem origem: autenticado por login ou token, declarado quando o cliente informou um dado e ele conferiu, ou inferido por coincidência de identificador. A regra que evita o vazamento é que vínculo fraco libera continuidade de assunto e nunca acesso a dado cadastral, financeiro ou ação com efeito colateral. Além disso, o vínculo inferido precisa expirar, porque o identificador parado por meses é o que tem maior chance de ter mudado de dono, e quando o mesmo identificador aparece ligado a duas identidades sem vínculo forte o sistema deve tratar como desconhecido e pedir confirmação de um dado que só o titular sabe, em vez de escolher a identidade mais recente. Vale também marcar no cadastro os identificadores compartilhados por natureza, como o WhatsApp de um setor, onde a inferência nunca é válida.

O que acontece se o cliente usar dois canais ao mesmo tempo?

É o caso que quebra a implementação ingênua, porque cada canal lê o estado compartilhado, modifica e escreve de volta, e a última escrita apaga a outra em silêncio. A saída que se paga é modelar o estado como fatos apensados com versão e carimbo de origem, em vez de um documento sobrescrito: cada canal escreve um fato novo e o conflito é resolvido na leitura, quando a janela é montada, aplicando a precedência do domínio. Fato autenticado vence fato inferido independentemente de qual chegou depois; entre fatos de mesma força vence o mais recente pelo relógio do servidor; e contradição direta em campo sensível, como endereço de entrega, não se resolve sozinha, marca o campo como em disputa e força confirmação no próximo turno. Duas consequências operacionais completam o desenho: chave de idempotência de ação com efeito colateral deve incluir a identidade e não o canal, senão a mesma ação disparada dos dois lados executa duas vezes, e o episódio aberto em outro canal precisa entrar na janela como estado, para que o bot reconheça o atendimento humano em curso em vez de competir com ele.

Continuidade é estado compartilhado, não histórico empilhado

Janela de contexto compartilhada entre canais não se resolve concatenando conversas, se resolve decidindo o que é fato durável, o que é resumo de episódio e o que é transcrição descartável, e montando a janela por orçamento com fatias reservadas para cada camada. Em volta disso, três decisões definem se o sistema ajuda ou vaza: vínculo de identidade com força explícita que limita o escopo exposto, estado apensado com precedência de domínio em vez de documento sobrescrito, e o reconhecimento de que o canal continua importando na hora de formatar e de confiar na entrada. Posso desenhar e implementar esse contexto compartilhado no seu atendimento com WhatsApp, web e telefone, para que o cliente pare de repetir a mesma história em cada canal sem que isso vire vazamento entre pessoas.