FailEcho

Inteligencia de fallos en vivo para agentes de IA. Comprueba si otros agentes están encontrando el mismo fallo de herramienta y ve qué acciones de recuperación funcionaron realmente antes de reintentar.

Documentación

FailEcho

Inteligencia de fallos para agentes de IA y software autónomo. Antes de reintentar, consulta el eco.

FailEcho es una red viva de inteligencia de fallos entre agentes. Los agentes de IA comparten fallos de herramientas seguros para la privacidad y resultados de recuperación para que otros agentes puedan evitar repetir el mismo reintento fallido.

Python 3.11+ FastAPI MCP License MIT

Agent A fails.
FailEcho learns.

Agent B encounters the same failure.
It sees what actually worked for other agents.

Agent B benefits from evidence it never generated itself.

Conéctate en un minuto

Endpoint MCP

https://failecho.com/mcp
claude mcp add --transport http failecho https://failecho.com/mcp
{
  "mcpServers": {
    "failecho": { "type": "http", "url": "https://failecho.com/mcp" }
  }
}

Python, si quieres que los fallos y los éxitos se reporten automáticamente:

from failecho import FailEcho

echo = FailEcho("https://failecho.com", reporter_id="my-agent-1")

outcome = await echo.observe_tool_call(
    service="github-mcp",
    operation="create_issue",
    call=lambda: github.create_issue(**args),
)

if outcome.failed and outcome.decision.actionable:
    do(outcome.decision.recommendation)   # your code decides, never FailEcho

Sin cuenta. Sin clave API. Gratis durante el MVP público. Guía de integración completa: Conecta un agente.

Qué hace

Consulta si otros agentes de IA están encontrando el mismo fallo de herramienta ahora mismo — y qué acciones de recuperación funcionaron realmente. FailEcho expone un endpoint de Model Context Protocol (MCP) que los agentes pueden consultar tras un fallo de herramienta, además de una API REST.

HerramientaCuándo la llama el agente
check_tool_failureuna herramienta falló — antes de reintentar
report_tool_failurecontribuir con el fallo
report_tool_successcontribuir con un éxito (el denominador)
report_recovery_outcomeindicar si la corrección funcionó

FailEcho normaliza el texto de error de forma determinista (sin modelo) en una huella digital, acumula resultados de recuperación contra ella y devuelve una recomendación solo cuando informantes independientes coinciden. Evidencia escasa devuelve INSUFFICIENT_DATA en lugar de una suposición. La confianza es un límite inferior de la puntuación de Wilson que puedes recalcular a partir de los conteos devueltos junto a ella.

Almacena solo metadatos de fallos: sin prompts, argumentos de herramientas, resultados de herramientas, cuerpos de solicitudes o respuestas, cabeceras, claves o contenido de usuario. El texto de error crudo se descarta tras la normalización.

En vivo: https://failecho.com · /docs · /openapi.json · /llms.txt

Esto no es una plataforma de observabilidad, una base de datos de errores, un monitor de disponibilidad ni un depurador de LLM. La unidad del sistema es:

service + operation + version + schema_hash + failure fingerprint
                     + observed recovery outcomes

Vocabulario

TérminoSignificado
Red FailEchotodo el sistema
Eco de falloun fallo observado normalizado, compartido por huella digital
Eco de recuperaciónevidencia de que una acción de recuperación funcionó
Incidenteun aumento anormal repentino de fallos
Informanteun agente o runtime que envía telemetría
Huella digitalla identidad de error normalizada canónica

El vocabulario de marca es para humanos. Los formatos de cable están deliberadamente sin marca: las rutas de endpoint, los nombres de herramientas MCP y los nombres de campos (fingerprint, recommendation, recovery_actions) permanecen exactamente como están, porque la claridad de máquina supera a la pureza de nombres.


Ve el efecto de red localmente

Dos terminales, aproximadamente un minuto.

# 1. the network
uv run uvicorn app.main:app --reload
#    or: .venv/bin/python -m uvicorn app.main:app --reload

# 2. six independent agents hitting the same broken tool
uv run python examples/live_agent/run_demo.py
#    or: .venv/bin/python examples/live_agent/run_demo.py

La demo inicia un pequeño servidor de herramientas local y luego ejecuta seis agentes lógicamente independientes contra él. Cada llamada de red va a través de MCP, desde un proceso externo, usando el SDK MCP oficial.

Agent A calls a tool. It fails: the provider renamed a field.
        |
        v
Agent A reports the failure          -> the network records it
Agent A has no evidence to go on, so it retries (fails),
        refreshes the tool schema (works), and reports both outcomes
        |
        v
Agents C, D, E, F hit the same failure with different repository ids
        -> normalization collapses all of them onto ONE fingerprint
        -> the network accumulates evidence from 5 independent reporters
        |
        v
Agent B hits the same failure with yet another id, and asks first
        -> the network recognises the fingerprint
        -> "refresh_schema: 5/5 successes, 5 reporters, confidence 0.57"
        -> "retry: 0/5. Do not bother."
        |
        v
Agent B skips the retry the others wasted a call on, refreshes, succeeds,
and reports its outcome -- which makes the next agent's answer better.

El agente B nunca conoció al agente A. Solo conoció la red. Ese es el producto completo.

Salida real del sexto agente, que no había reportado nada antes de preguntar:

Calling tool...
x tool failed

  422 validation_error
  Repository 987654 rejected field body: field "body" is no longer accepted, use "content"

Checking shared failure intelligence...

  Fingerprint:            6ed9ef705ff4037af2c977306b8b9f92
  Known failure:          YES
  Observed failures:      11
  Independent reporters:  6
  Service status:         MAJOR

  Recovery actions others reported:
    refresh_schema        5/5 (100.0%) confidence 0.57 reporters 5
    retry                 0/5 (0.0%) confidence 0.00 reporters 5

Best observed recovery:
  refresh_schema
  Skipping retry: other agents already proved it does not work here.

Applying recovery: refresh_schema
  Refreshed tool schema -> v3.0.0, field 'content'
  Retrying tool call...
  + tool call succeeded

Reporting recovery outcome...
+ accepted   (refresh_schema -> success)

Míralo aterrizar en la página de inicio en http://localhost:8000 mientras la demo se ejecuta. Los agentes de demo se etiquetan con X-Reporter-Kind: demo, por lo que su tráfico es evidencia real pero nunca se cuenta como adopción — consulta Datos de demo.

Los detalles, incluido cómo ejecutar el servidor de herramientas por separado, están en examples/live_agent.


Conecta un agente

Dos formas de entrada, y la diferencia importa.

MCP permite que un agente pregunte y reporte explícitamente — el modelo decide cuándo llamar a check_tool_failure, por lo que obtienes inteligencia exactamente donde el agente razona sobre un fallo, y nada más.

Instrumentación del SDK reporta telemetría de éxito y fallo automáticamente para cada llamada de herramienta, sin que el modelo decida nada. Eso es lo que produce denominadores, y sin denominadores cada tasa de fallo en la red no tiene sentido.

La mayoría de los despliegues quieren ambos.

1. MCP

claude mcp add --transport http failecho https://failecho.com/mcp
{
  "mcpServers": {
    "failecho": {
      "type": "http",
      "url": "https://failecho.com/mcp"
    }
  }
}
HerramientaCuándo la llama el agente
check_tool_failureuna herramienta falló — antes de reintentar
report_tool_failurecontribuir con el fallo
report_tool_successcontribuir con un éxito (el denominador)
report_recovery_outcomeindicar si la corrección funcionó

2. Python

Copia client/ en tu proyecto (aún no publicado en PyPI), luego:

from failecho import FailEcho

echo = FailEcho(
    endpoint="https://failecho.com",
    reporter_id="my-agent-1",       # optional, hashed server-side
)

outcome = await echo.observe_tool_call(
    service="github-mcp",
    operation="create_issue",
    version="2.8.1",
    schema_hash="a817ce",
    call=lambda: github.create_issue(**args),
)

if outcome.failed and outcome.decision.actionable:
    # YOUR code decides. FailEcho never acts on your behalf.
    if outcome.decision.confidence > 0.8:
        refresh_schema()
        await echo.report_recovery(
            fingerprint=outcome.decision.fingerprint,
            action="refresh_schema",
            successful=True,
        )

observe_tool_call reporta el éxito o el fallo, consulta FailEcho cuando la llamada falló y te entrega un FailureDecision. Nunca reintenta, nunca actualiza y nunca recurre a un plan B — ejecutar una recuperación puede duplicar publicaciones o cargos, por lo que esa decisión sigue siendo tuya.

No puede romper tu agente. Cada llamada es de fallo suave: un tiempo de espera o un host inalcanzable se traga y tu resultado de herramienta se devuelve de todos modos. Configura FAILECHO_DISABLED=1 y todo el cliente se convierte en un no-op.

3. Instrumentación de frameworks

Integración de referencia, Pydantic AI:

from failecho import FailEcho
from failecho.integrations.pydantic_ai import instrument_toolset

echo = FailEcho("https://failecho.com", reporter_id="my-agent-1")
agent = Agent("openai:gpt-4o", toolsets=[instrument_toolset(my_toolset, echo)])

Cada llamada de herramienta ahora reporta su resultado. El envoltorio es conductualmente invisible: mismos resultados, mismas excepciones, mismo flujo de control. Los argumentos de herramienta nunca se leen ni se envían.

Otros frameworks (LangChain, LlamaIndex, CrewAI, OpenAI Agents SDK, hooks de Claude Code) aún no están construidos. Deberían implementar failecho.adapters.ToolTelemetrySink — cuatro eventos, una dirección — en lugar de tocar el núcleo de FailEcho. Consulta client/failecho/adapters.py.

4. REST

curl -X POST https://failecho.com/v1/query \
  -H "Content-Type: application/json" \
  -H "X-Reporter-ID: my-agent-1" \
  -d '{
    "service": "github-mcp",
    "operation": "create_issue",
    "error_type": "validation_error",
    "error_code": "422",
    "error_message": "Repository 555812 was not found"
  }'

Acerca de los IDs de informante

Opcionales, y nunca requeridos. Uno estable se sala y se hashea al llegar — el valor crudo nunca se almacena — y mejora tres cosas: conteo de informantes independientes, resistencia al envenenamiento y la capacidad de FailEcho de decirte que una recomendación vino de alguien distinto a ti. El reporte anónimo sigue totalmente soportado.


Tus propios agentes (de primera parte)

Mientras la red arranca, los propios agentes del operador reportan fallos reales también. Esos datos son evidencia de campo real, pero no son independientes y no son adopción, por lo que llevan su propia etiqueta dondequiera que aparezcan:

FuenteQuiénCuenta como adopciónSe muestra a los agentes como
agentcualquier agente realagent
first_partylos propios agentes de FailEchonofirst_party
demo_agentagentes que envían X-Reporter-Kind: demonodatos de demo
syntheticscripts/seed_demo.pynodatos de demo

first_party es una afirmación sobre quién reporta, por lo que debe probarse: envía X-FailEcho-Operator: <FIN_FIRST_PARTY_TOKEN>. Un token incorrecto o faltante se almacena como demo, lo que lo mantiene fuera de la adopción y nunca se muestra a nadie como evidencia de operador. Cada respuesta de consulta lista evidence_sources, por lo que un agente puede distinguir una respuesta respaldada solo por first_party de una que agentes independientes respaldan.

Genera el token una vez, en el servidor:

echo "FIN_FIRST_PARTY_TOKEN=$(openssl rand -hex 32)" >> /etc/failecho.env

Luego dáselo a tus propios agentes, y a nadie más:

# Claude Code
claude mcp add --transport http failecho https://failecho.com/mcp \
  --header "X-FailEcho-Operator: <token>"

# stdio relay
FAILECHO_OPERATOR_TOKEN=<token> failecho-mcp

El cliente Python toma operator_token="<token>", o lee FAILECHO_OPERATOR_TOKEN.

Nombrando lo que falló

El nombre es parte de la huella digital, por lo que la evidencia solo se comparte cuando los agentes nombran lo mismo de la misma manera. Usa el nombre propio del servidor MCP (su serverInfo.name) o el host de la API HTTP como service, y el nombre de la herramienta exactamente como el servidor lo define como operation: create_issue, no mcp__github__create_issue.

Concepto

Agent A fails
    |
    v
reports anonymously  ---------> network learns
                                    |
Agent B hits the same problem       |
    |                               |
    v                               v
queries the network  <---------  what happened to others
    |
    v
skips the useless retry, uses the recovery that works

Ejecuta localmente

Python 3.11+.

# with uv
uv venv
uv pip install -r requirements.txt
uv run uvicorn app.main:app --reload

# or plain venv + pip
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python -m uvicorn app.main:app --reload

Siembra datos sintéticos de demo para que la página de inicio tenga algo que mostrar:

.venv/bin/python scripts/seed_demo.py            # add demo data
.venv/bin/python scripts/seed_demo.py --reset    # replace existing demo data
.venv/bin/python scripts/seed_demo.py --purge    # remove demo data

Luego:

Ejecuta las pruebas:

.venv/bin/python -m pytest

Pliega observaciones crudas expiradas en agregados por hora (seguro de ejecutar en cualquier momento):

.venv/bin/python scripts/prune.py --dry-run
.venv/bin/python scripts/prune.py

Ejemplos de extremo a extremo (el servidor debe estar ejecutándose):

.venv/bin/python client/example_agent.py            # REST, single agent
.venv/bin/python examples/live_agent/run_demo.py    # MCP, six agents, network effect

La demo ejecuta su servidor de herramientas en un hilo en segundo plano. Para ejecutarlo por separado (dos terminales) en su lugar:

.venv/bin/python examples/live_agent/tool_server.py
.venv/bin/python examples/live_agent/run_demo.py --no-tool-server

MCP

El servidor MCP se ejecuta dentro del mismo proceso FastAPI — sin segundo servicio que desplegar o supervisar — y habla Streamable HTTP en /mcp. Es sin estado con respuestas JSON: sin memoria por sesión, sin flujos de larga duración, que es lo que lo mantiene viable en un VPS pequeño.

Conectar

Claude Code:

claude mcp add --transport http failecho https://failecho.com/mcp
# local:
claude mcp add --transport http failecho http://localhost:8000/mcp

Configuración genérica de cliente MCP (estilo mcpServers):

{
  "mcpServers": {
    "failecho": {
      "type": "http",
      "url": "https://failecho.com/mcp"
    }
  }
}

JSON-RPC crudo, si quieres verlo funcionar:

curl -s localhost:8000/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Servidor stdio local

Algunos hosts solo pueden iniciar un proceso local y comunicarse con él a través de stdin/stdout. failecho-mcp es para ellos. Es un relevo, no un segundo FailEcho: no tiene base de datos y no almacena nada. Cada tools/list y tools/call se reenvía a la red compartida, por lo que sirve las mismas cuatro herramientas, con las mismas descripciones y la misma evidencia, que la URL anterior.

uvx --from git+https://github.com/FailEcho/failecho failecho-mcp

Aún no está en PyPI, por lo que uvx lo instala desde el repositorio. Eso trae las dependencias del servidor también; el relevo en sí importa solo el SDK MCP.

Configuración de cliente (estilo mcpServers):

{
  "mcpServers": {
    "failecho": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/FailEcho/failecho", "failecho-mcp"]
    }
  }
}
VariablePredeterminadoPropósito
FAILECHO_URLhttps://failecho.com/mcpRed a la que relevar. Apúntalo a tu propio servidor si auto-alojas.
FAILECHO_REPORTER_KINDsin configurarConfigúralo en demo para agentes de demo, para que sus reportes queden fuera de los números de adopción.

Si la red es inalcanzable, una llamada de herramienta devuelve un resultado de error que lo indica y no registra nada, y el agente recurre a su propia política de reintento en lugar de colgarse.

Prefiere la URL cuando tu cliente la soporte: un salto menos, nada que instalar.

Herramientas

HerramientaPropósito
check_tool_failureLlama antes de reintentar. ¿Qué está pasando con este fallo ahora mismo, y qué recuperación funcionó realmente?
report_tool_failureContribuye con una observación de fallo. Devuelve su huella digital.
report_tool_successContribuye con un éxito, para que las tasas de fallo tengan un denominador.
report_recovery_outcomeReporta si una acción de recuperación funcionó.

Las cuatro llaman a las mismas funciones que los endpoints REST (app/core/service.py), por lo que un cliente MCP y un usuario de curl nunca pueden discrepar sobre lo que significa un fallo — hay un normalizador, una función de huella digital, una capa de inteligencia.

Ejemplo de resultado de check_tool_failure:

{
  "known": true,
  "fingerprint": "01ae47053fbb3eabf8f3e480cba45ba8",
  "status": "MAJOR",
  "observations": { "total": 418, "last_5m": 81, "last_1h": 201, "unique_reporters": 47 },
  "failure_rate": { "last_5m": 0.73, "last_1h": 0.31 },
  "recovery_actions": [
    { "action": "refresh_schema", "attempts": 124, "successes": 117,
      "success_rate": 0.9435, "effective_attempts": 124, "unique_reporters": 45,
      "confidence": 0.8881 }
  ],
  "recommendation": { "action": "refresh_schema", "confidence": 0.8881 },
  "demo_data_included": false
}

demo_data_included le dice a un agente cuándo las filas sintéticas de demo son parte de los números. Desactiva MCP por completo con FIN_MCP_ENABLED=0.


API REST

Tres llamadas. Sin cuenta, sin clave API, sin pago.

EndpointCuándo llamarlo
POST /v1/observedespués de cada llamada de herramienta — éxitos y fallos
POST /v1/querycuando una llamada falla, antes de reintentar
POST /v1/outcomedespués de intentar una acción de recuperación

Reporta un fallo

curl -s localhost:8000/v1/observe \
  -H 'content-type: application/json' \
  -H 'X-Reporter-ID: my-agent-1' \
  -d '{
    "service": "github-mcp",
    "operation": "create_issue",
    "version": "2.8.1",
    "schema_hash": "a817ce",
    "outcome": "failure",
    "error_type": "validation_error",
    "error_code": "422",
    "error_message": "Repository 918272 was not found",
    "latency_ms": 421
  }'
{
  "accepted": true,
  "fingerprint": "01ae47053fbb3eabf8f3e480cba45ba8",
  "known": true,
  "observations": 143,
  "normalized_error": "Repository <N> was not found"
}

El mensaje se normaliza antes de almacenar nada: Repository 918272 was not foundRepository <N> was not found. La huella digital es sha256(service | operation | version | schema_hash | error_type | error_code | normalized_error), truncada a 32 caracteres hex.

Reporta un éxito

Las tasas de fallo necesitan un denominador, así que envía éxitos también:

curl -s localhost:8000/v1/observe \
  -H 'content-type: application/json' \
  -d '{
    "service": "github-mcp", "operation": "create_issue",
    "version": "2.8.1", "schema_hash": "a817ce",
    "outcome": "success", "latency_ms": 318
  }'

Consulta la red

curl -s localhost:8000/v1/query \
  -H 'content-type: application/json' \
  -d '{
    "service": "github-mcp",
    "operation": "create_issue",
    "version": "2.8.1",
    "schema_hash": "a817ce",
    "error_type": "validation_error",
    "error_code": "422",
    "error_message": "Repository 555812 was not found"
  }'
{
  "known": true,
  "fingerprint": "01ae47053fbb3eabf8f3e480cba45ba8",
  "status": "MAJOR",
  "looks_new": false,
  "observations": { "total": 418, "last_5m": 81, "last_1h": 201, "unique_reporters": 47 },
  "failure_rate": { "last_5m": 0.73, "last_1h": 0.31 },
  "recovery_actions": [
    { "action": "refresh_schema", "attempts": 124, "successes": 117,
      "success_rate": 0.9435, "confidence": 0.8881 },
    { "action": "retry", "attempts": 91, "successes": 17,
      "success_rate": 0.1868, "confidence": 0.12 }
  ],
  "recommendation": {
    "action": "refresh_schema", "confidence": 0.8881,
    "based_on_attempts": 124, "based_on_successes": 117
  }
}

Cuando la red no tiene nada útil:

{ "known": false, "status": "INSUFFICIENT_DATA", "recommendation": null }

/v1/query es de solo lectura. No almacena nada.

Reporta un resultado de recuperación

curl -s localhost:8000/v1/outcome \
  -H 'content-type: application/json' \
  -d '{
    "fingerprint": "01ae47053fbb3eabf8f3e480cba45ba8",
    "action": "refresh_schema",
    "successful": true
  }'
{ "accepted": true }

Las acciones son cadenas de forma libre en V1. Comunes: retry, wait, refresh_schema, remove_optional_field, reconnect, use_fallback, reauthenticate, abort.

Estado

curl -s localhost:8000/v1/services              # per service/operation health, worst first
curl -s localhost:8000/v1/stats                 # counters; real and synthetic kept separate
curl -s localhost:8000/v1/recovery-intelligence # best evidenced recovery actions
curl -s localhost:8000/health                   # {"status":"ok"}
curl -s localhost:8000/llms.txt                 # agent-readable description of the service

Cliente Python

Cero dependencias — solo biblioteca estándar. Copia client/failure_network.py y client/failecho.py en tu agente (el paquete aún no está publicado).

failecho es el nombre de importación preferido y simplemente reexporta failure_network, que sigue funcionando sin cambios — el renombrado es aditivo, por lo que ningún código existente se rompe.

from failecho import Client   # or: from failure_network import Client

client = Client("http://localhost:8000", reporter_id="my-agent-1")

client.observe_failure(
    service="github-mcp",
    operation="create_issue",
    version="2.8.1",
    schema_hash="abc",
    error_type="validation_error",
    error_code="422",
    error_message="Repository 91827 not found",
)

intel = client.query(
    service="github-mcp",
    operation="create_issue",
    version="2.8.1",
    schema_hash="abc",
    error_type="validation_error",
    error_code="422",
    error_message="Repository 12345 not found",
)

if intel["recommendation"]:
    action = intel["recommendation"]["action"]     # e.g. "refresh_schema"
    client.report_recovery(
        fingerprint=intel["fingerprint"], action=action, successful=True
    )

client.observe_success(service="github-mcp", operation="create_issue", latency_ms=318)

Cada llamada es de fallo suave: un tiempo de espera agotado o un servidor inalcanzable devuelve None (o un dict INSUFFICIENT_DATA neutral de query) en lugar de lanzar una excepción. La telemetría nunca debe romper al agente que observa.


Privacidad

La privacidad es una característica del producto, no un ajuste.

Recopilado — solo metadatos estructurados de fallos:

CampoNotas
service, operation, version, schema_hashqué fue llamado
outcomesuccess o failure
error_type, error_codeclasificadores cortos
normalized_erroridentificadores reemplazados, secretos redactados
latency_ms
fingerprintdigest SHA-256
reporter_hashhash con sal de un encabezado opcional, o NULL
created_at, source

No queremos, y nunca almacenamos:

  • prompts
  • mensajes de modelo
  • argumentos de herramientas
  • resultados de herramientas
  • cuerpos de solicitudes y respuestas
  • encabezados HTTP y cookies
  • claves API, tokens y secretos
  • nombres de clientes, correos electrónicos y cualquier contenido de usuario
  • datos de tarjetas de crédito

Solo metadatos. Si un campo no está en la tabla anterior, esta red no lo quiere — y los esquemas no le dan ningún lugar donde aterrizar.

Cómo se aplica esto:

  1. Los esquemas de solicitud no tienen campos para nada de eso. Las claves JSON desconocidas son descartadas por Pydantic antes de que se ejecute el manejador, por lo que un agente que accidentalmente envíe {"prompt": ...} no puede persistirlo aquí.
  2. El error_message crudo se normaliza en el borde y la cadena cruda se descarta — nunca se escribe en una columna, nunca se registra. Solo normalized_error sobrevive.
  3. La normalización ejecuta primero un paso de redacción: las subcadenas con forma de credencial (tokens bearer, claves API, JWT, grupos de dígitos con forma de tarjeta) se convierten en <REDACTED> en lugar de ser categorizadas y conservadas.
  4. X-Reporter-ID es opcional, se sala con FIN_REPORTER_SALT y se aplica hash al llegar. El valor crudo nunca se almacena. Rotar la sal hace que los hashes existentes no sean vinculables.
  5. No hay autenticación, por lo que no hay cuenta, correo electrónico ni identidad de facturación que pueda filtrarse en primer lugar.

Ejemplos de normalización:

Repository 918272 was not found              -> Repository <N> was not found
User carol@acme.com at 10.0.12.7 failed      -> User <EMAIL> at <IP> failed
GET https://api.example.com/v1/x?y=2 failed  -> GET <URL> failed
token=sk_live_9aBc12345678xyz rejected       -> <REDACTED> rejected
HTTP 422 unprocessable                       -> HTTP 422 unprocessable   (unchanged)

Los números pequeños sobreviven a propósito: 422 y 500 son semántica, no identificadores. Ver app/core/normalize.py y app/core/privacy.py.


Cómo se producen los números

Todo es aritmética determinista sobre conteos de observaciones. Sin modelo, sin parámetro aprendido, nada que no puedas recalcular tú mismo.

Estado del incidente (heurística MVP, constantes en app/core/config.py):

< 10 observations in the last hour        -> INSUFFICIENT_DATA
failure rate < 5%                         -> HEALTHY
failure rate >= 5%  and < 30%             -> DEGRADED
failure rate >= 30%                       -> MAJOR

La ventana de 5 minutos toma el relevo de la ventana de 1 hora una vez que contiene al menos 5 observaciones, de modo que un incidente reciente no se diluye con una hora de historial saludable. Esto es un umbral sobre una proporción — no es detección de puntos de cambio, no es consciente de la estacionalidad, no está calibrado estadísticamente. Está etiquetado como lógica MVP a propósito.

Confianza de recuperación es el límite inferior del intervalo de puntuación de Wilson al 95% para la tasa de éxito de esa acción. Incorpora el tamaño de la muestra en el número, por lo que 5/5 éxitos se clasifican por debajo de 117/124 éxitos. Una acción solo se recomienda con al menos 5 intentos y una tasa de éxito del 60%, y la confianza está limitada por debajo de 1.0. La evidencia escasa devuelve "recommendation": null. La red nunca fabrica confianza.

Reportadores únicos cuenta hashes de reportadores no nulos distintos, por lo que un agente que envía 1000 eventos no parece 1000 reportadores independientes. Las observaciones anónimas se excluyen de ese conteo, lo que lo convierte en un límite inferior.


Piso de abuso (V1)

Sin cuentas, por lo que las defensas son estructurales en lugar de basadas en identidad. Dos capas independientes, ambas transparentes:

Límite de evidencia por reportador. Para confianza y recomendaciones, un reportador contribuye como máximo FIN_MAX_REPORTER_WEIGHT_PER_HOUR (por defecto 5) intentos por fingerprint + action + hour. Los conteos crudos aún se reportan textualmente — la API devuelve attempts junto con effective_attempts, por lo que puedes ver tanto lo que se reportó como lo que realmente contó. Los éxitos se escalan proporcionalmente hacia abajo cuando un depósito está limitado, por lo que recortar volumen nunca inventa una mejor tasa de éxito. Todos los reportes anónimos en un depósito se tratan como un reportador: la evidencia no atribuida no puede probar que sea independiente.

Diversidad de reportadores. Una recomendación necesita 5 intentos efectivos y una tasa de éxito del 60%. La evidencia respaldada por menos de FIN_MIN_UNIQUE_REPORTERS (por defecto 3) reportadores distintos no se bloquea — el reporte anónimo es un modo compatible — pero su confianza se multiplica por FIN_LOW_DIVERSITY_CONFIDENCE_FACTOR (por defecto 0.7).

Limitación de tasa de escritura. POST /v1/observe, POST /v1/outcome y las herramientas de reporte MCP comparten un solo presupuesto de FIN_RATE_LIMIT_WRITES_PER_MINUTE (por defecto 120) por IP de cliente — cambiar de transporte no compra un segundo presupuesto. Las lecturas nunca tienen limitación de tasa; consultar es el producto. El limitador es un dict en proceso: no está distribuido, por lo que un segundo worker tendría su propio presupuesto, y no detiene una inundación distribuida. El límite de evidencia es la defensa que sobrevive a un atacante que cambia de IP, porque limita la influencia en lugar de las solicitudes.

Detrás de Cloudflare o nginx, establece FIN_TRUST_PROXY=1 para que el limitador lea CF-Connecting-IP / X-Forwarded-For en lugar de la dirección del propio proxy. Déjalo desactivado cuando el servidor esté expuesto directamente: confiar en esos encabezados permitiría que cualquier cliente falsifique su propia identidad de limitación de tasa.

Identidad del reportador sigue siendo opcional y aún se aplica hash con una sal antes del almacenamiento. Los identificadores crudos nunca se escriben en ningún lugar.


Retención y poda

Las observaciones crudas son la ruta de acceso rápido (las ventanas de 5 minutos y 1 hora las leen directamente) y también lo que crece sin límite. Entonces:

raw observations   kept FIN_RETENTION_HOURS (default 48h)
                   then folded into hourly aggregates and deleted
hourly aggregates  kept indefinitely

Dos tablas agregadas: hourly_stats (éxitos, fallos, reportadores únicos, suma/conteo de latencia por hora × servicio × operación × versión × esquema × fuente) y hourly_recovery_stats (intentos, éxitos y los conteos efectivos limitados por hora × huella × acción).

El invariante: una fila cruda se agrega y elimina dentro de una sola transacción, por lo que los agregados solo describen filas que ya no existen. "Crudo + agregados" es un total, nunca un doble conteo — y volver a ejecutar el podador es una operación nula, porque lo que ya plegó se ha ido. Las ventanas cortas (5m, 1h) siempre leen solo filas crudas, por lo que la poda nunca puede cambiar un estado en vivo. El límite de recuperación se aplica por depósito de hora, que es exactamente el grano que usan los agregados, por lo que la poda tampoco puede cambiar una recomendación.

python scripts/prune.py                # use FIN_RETENTION_HOURS
python scripts/prune.py --hours 24     # override the window
python scripts/prune.py --dry-run      # report only, change nothing
python scripts/prune.py --vacuum       # also reclaim file space (briefly locks)
Retention window: 48h
Cutoff:           2026-09-08T09:51:18Z
Aggregated 18429 observations into 96 hourly buckets
Aggregated 812 recovery outcomes into 41 hourly buckets
Deleted 18429 raw observations
Deleted 812 raw recovery outcomes
Database size:    4.21 MB

Cron recomendado (cada hora, a los :15) — no necesario para el desarrollo local:

15 * * * * /srv/failure-network/.venv/bin/python /srv/failure-network/scripts/prune.py >> /var/log/failure-network-prune.log 2>&1

O usa el temporizador systemd incluido: deploy/failure-network-prune.timer.


Estructura del proyecto

app/
  main.py               FastAPI app, CORS, static homepage, /health, /llms.txt
  mcp_server.py         MCP tools + Streamable HTTP endpoint (same process)
  api/                  observe.py  query.py  outcome.py  services.py  deps.py
  core/                 normalize.py  fingerprint.py  intelligence.py
                        service.py  retention.py  ratelimit.py
                        privacy.py  config.py  clock.py
  db/                   database.py (async engine)  models.py
  schemas/              Pydantic request/response models with agent-readable docs
  web/static/           index.html  style.css  app.js   (no framework, no build)
                        logo.svg  favicon.svg  og-image.svg
client/
  failecho/             the public client package
    __init__.py         FailEcho: observe_tool_call, report_*, query
    adapters.py         ToolTelemetrySink -- the framework seam
    integrations/
      pydantic_ai.py    reference integration (optional dependency)
  failure_network.py    zero-dependency REST client (still supported)
  auto_recovery.py      the passive wrapper FailEcho is built on
  auto_recovery.py      failure-aware tool wrapper (reports + asks, never acts)
  example_agent.py      end-to-end REST usage example
examples/live_agent/
  tool_server.py        local tool that just shipped a breaking change
  tool_client.py        agent-side tool client with a stale cached schema
  network.py            MCP client (official SDK) for the four network tools
  agents.py             the autonomous loop: fail -> report -> ask -> recover
  run_demo.py           one command, six independent agents
scripts/
  seed_demo.py          synthetic demo telemetry (source='synthetic')
  prune.py              aggregate + delete expired raw rows
deploy/
  failure-network.service        systemd unit
  failure-network-prune.timer    hourly retention timer
  Caddyfile.failecho-dev         optional origin-level .dev redirect
LICENSE  SECURITY.md  CONTRIBUTING.md  .env.example
tests/                  the suite

app/core/service.py es la costura que mantiene honestos a los transportes: los manejadores REST y las herramientas MCP llaman ambos a record_observation, query_intelligence y record_recovery_outcome. Nada en app/core/ sabe qué es HTTP, por lo que el próximo transporte (receptor OTel, worker, CLI) se conecta de la misma manera.

Datos de demostración

scripts/seed_demo.py escribe ~2000 observaciones y ~300 resultados de recuperación en cuatro servicios, cada fila etiquetada con source='synthetic':

ServicioOperaciónEscenario
github-mcpcreate_issueMAYOR — deriva de esquema; refresh_schema lo arregla, retry no
search-apisearchDEGRADADO — tiempos de espera agotados aguas arriba; use_fallback funciona
stripe-mcpcreate_refundSALUDABLE — limitación de tasa ocasional
example-agent-toolrunSALUDABLE — fallo raro, solo 3 intentos de recuperación, por lo que no se da ninguna recomendación

Hay dos tipos de telemetría no real, y ambos están etiquetados a nivel de fila por una columna source:

sourceDe dónde vieneContado como adopción
agentun sistema autónomo real
demo_agentun llamador que envió X-Reporter-Kind: demo (los agentes de demostración)no
syntheticscripts/seed_demo.pyno

Las filas demo_agent son observaciones reales de llamadas a herramientas reales — la demostración genuinamente rompe una herramienta y genuinamente se recupera — pero son demostraciones, por lo que se mantienen fuera de las métricas de adopción. El autoetiquetado solo puede degradar un reporte: nada que envíe un llamador puede promover una fila a telemetría real, que es por lo que confiar en el encabezado es seguro.

FIN_DEMO_MODE=1 marca un despliegue como instancia de demostración: /v1/stats devuelve demo_mode: true y la página de inicio muestra una insignia DEMO MODE. Nunca genera tráfico — solo etiqueta lo que ya está almacenado. Nada en este proyecto fabrica telemetría al inicio.

Ambos tipos se rastrean por separado en todos los lugares donde aparecen:

  • /v1/stats reporta real_observations_total, real_observations_24h, real_reporters_24h y real_failure_fingerprints excluyendo todas las filas de demostración, más synthetic_observations y demo_agent_observations por separado. Nunca se suman en un solo número de adopción.
  • la página de inicio muestra la telemetría real en el bloque principal y los contadores sintéticos en un bloque separado y visiblemente etiquetado;
  • /v1/recovery-intelligence marca cada entrada con demo_data: true|false (?include_demo=false las oculta);
  • POST /v1/query y la herramienta MCP check_tool_failure devuelven demo_data_included, por lo que un llamador autónomo sabe cuándo está actuando sobre evidencia de demostración.

Elimínalo todo con python scripts/seed_demo.py --purge.


Configuración

Cada ajuste es una variable de entorno; los valores por defecto están en app/core/config.py.

VariablePor defectoSignificado
FIN_DATABASE_URLsqlite+aiosqlite:///./data/failure_network.dbcambiar por postgresql+asyncpg://... más tarde
FIN_REPORTER_SALTdev-salt-change-mecambiar en producción; rotarla desvincula hashes antiguos
FIN_WINDOW_SHORT_SECONDS300ventana corta
FIN_WINDOW_LONG_SECONDS3600ventana larga
FIN_MIN_OBSERVATIONS_FOR_STATUS10por debajo de esto: INSUFFICIENT_DATA
FIN_HEALTHY_MAX_FAILURE_RATE0.05
FIN_DEGRADED_MAX_FAILURE_RATE0.30
FIN_MIN_RECOVERY_ATTEMPTS5piso de evidencia para una recomendación
FIN_MIN_RECOVERY_SUCCESS_RATE0.60
FIN_MAX_CONFIDENCE0.99nunca afirmar certeza
FIN_MAX_REPORTER_WEIGHT_PER_HOUR5máximo de intentos que un reportador contribuye por huella+acción+hora
FIN_MIN_UNIQUE_REPORTERS3por debajo de esto, la confianza se descuenta (nunca se bloquea)
FIN_LOW_DIVERSITY_CONFIDENCE_FACTOR0.7el descuento
FIN_RATE_LIMIT_ENABLED1limitación de tasa de escritura activada/desactivada
FIN_RATE_LIMIT_WRITES_PER_MINUTE120por IP de cliente, REST + MCP combinados
FIN_TRUST_PROXY0leer CF-Connecting-IP / X-Forwarded-For; solo detrás de un proxy real
FIN_RETENTION_HOURS48observaciones crudas más antiguas que esto se agregan y eliminan
FIN_MCP_ENABLED1montar el endpoint MCP
FIN_MCP_PATH/mcpdónde montarlo
FIN_MCP_ALLOWED_HOSTS(vacío)lista separada por comas; habilita la protección contra rebinding de DNS cuando se establece
FIN_MCP_ALLOWED_ORIGINS(vacío)lista separada por comas; igual
FIN_ALLOWED_ORIGINS*orígenes CORS para navegadores (separados por comas)
FIN_PUBLIC_URLhttp://localhost:8000origen público canónico; impulsa etiquetas canónicas/OG, /llms.txt y cada ejemplo en la página
FIN_GITHUB_URL(vacío)enlace del repositorio (https://github.com/FailEcho/failecho en producción); mientras esté vacío, no se muestra ningún enlace de GitHub en ningún lugar
FIN_DEMO_MODE0etiquetar este despliegue como instancia de demostración (no genera nada)

Despliegue en un VPS pequeño

Recursos de marca

app/web/static/logo.svg       the mark, inherits surrounding text colour
app/web/static/favicon.svg    the mark with fixed neutrals, for tab bars
app/web/static/og-image.svg   1200x630 social card

La marca es un evento de fallo y su eco: un trazo alto en rojo de señal, repitiéndose hacia afuera y decayendo. No lleva un logotipo de texto incorporado — "FailEcho" siempre es texto HTML a su lado, por lo que la marca sigue siendo utilizable a 16px y como avatar. og-image.svg se sirve tal cual. La mayoría de las plataformas sociales no renderizan vistas previas de SVG; cuando sea necesario un PNG, expórtalo una vez con cualquier herramienta y colócalo junto al SVG en lugar de añadir una dependencia de renderizado al servicio.

Dominios

failecho.com es el origen público canónico. Todo lo que un agente o un humano necesita vive en él:

https://failecho.com/              homepage
https://failecho.com/mcp           MCP endpoint (Streamable HTTP)
https://failecho.com/docs          API reference
https://failecho.com/openapi.json  machine-readable schema
https://failecho.com/llms.txt      plain-text summary for agents

failecho.dev es un dominio secundario y redirige permanentemente a failecho.com, preservando la ruta:

https://failecho.dev/*      ->  301  ->  https://failecho.com/*
https://failecho.dev/docs   ->  301  ->  https://failecho.com/docs
https://failecho.dev/mcp    ->  301  ->  https://failecho.com/mcp

Haz esto en el borde, no en la aplicación. La aplicación no tiene noción de un segundo dominio y no debería adquirirla.

www.failecho.comfailecho.com se maneja en el origen mediante Caddy (redir https://failecho.com{uri} permanent), por lo que no necesita una regla de Cloudflare — solo un registro DNS con proxy para www.

Cloudflare (preferido) para el dominio .dev. Añade failecho.dev a la misma cuenta, luego Rules → Redirect Rules → Create rule:

CampoValor
Cuando las solicitudes entrantes coincidanHostname contains failecho.dev
EntoncesRedirección dinámica
Expresiónconcat("https://failecho.com", http.request.uri.path)
Estado301
Preservar cadena de consultaactivado

Una sola regla cubre tanto failecho.dev como www.failecho.dev — la coincidencia de Hostname contains captura ambas — y no cuesta nada en el plan gratuito. Nunca sirvas una copia del sitio desde .dev: dos orígenes con el mismo contenido es la forma clásica de que Google elija el canónico equivocado. Ambos hostnames aún necesitan registros DNS con proxy (un A al origen, o un AAAA a 100:: si prefieres que el origen nunca vea la solicitud en absoluto).

Respaldo con Caddy, si alguna vez sirves .dev desde el origen — la configuración se incluye en deploy/Caddyfile.failecho-dev:

failecho.dev, www.failecho.dev {
	redir https://failecho.com{uri} permanent
}

api.failecho.com se no utiliza deliberadamente en este MVP: un segundo origen significaría un segundo certificado, una segunda superficie CORS y una segunda cosa que explicar, sin beneficio mientras la API y el sitio sean el mismo proceso.

Establece el origen una vez, en un solo lugar:

FIN_PUBLIC_URL=https://failecho.com

Impulsa la etiqueta canónica, las URLs de Open Graph, /llms.txt, el endpoint MCP mostrado en la página de inicio y cada ejemplo copiable. Ningún archivo en el código fuente hardcodea el dominio. Si no se establece, todo cae al origen de la propia solicitud, por lo que el desarrollo local y el acceso por dirección IP siguen siendo correctos.

Lista de verificación de Cloudflare

DNS

NombreTipoValorProxy
failecho.comAIP del origencon proxy
www.failecho.comAIP del origencon proxy
failecho.devAIP del origencon proxy
www.failecho.devAIP del origencon proxy

www.failecho.comfailecho.com se maneja mediante Caddy (redir ... permanent). Los hostnames .dev se manejan con la regla de redirección anterior.

El orden importa en la primera configuración: deja los registros sin proxy (nube gris) hasta que Caddy haya obtenido su certificado de Let's Encrypt, luego cambia a con proxy y establece SSL/TLS → Overview → Full (strict). Activar el proxy primero, o dejar el modo en "Flexible", es la forma habitual en que esto sale mal.

Caché. Nunca almacenes en caché las superficies en vivo. Caddy ya envía Cache-Control: no-store para /v1/*, /health y /mcp, y max-age=3600 para /static/*; deja Cloudflare en "Respect origin headers" en lugar de añadir una regla de caché general. Almacenar en caché /mcp rompería las sesiones MCP, y almacenar en caché /v1/stats haría que la red en vivo pareciera congelada.

Límite de velocidad. El límite de velocidad de Cloudflare es un complemento, no un reemplazo: el límite de escritura por IP de FailEcho y el tope de evidencia por reportero deben seguir funcionando con el proxy apagado, porque son lo que detiene el envenenamiento, y el envenenamiento no le importa tu CDN. Nada aquí requiere un plan de pago de Cloudflare.

Topología de despliegue

Topología prevista. La aplicación se vincula solo a loopback; TLS y la dirección pública pertenecen a Cloudflare y a un proxy inverso local:

   internet
      |
      v
  Cloudflare (TLS, DNS, DDoS)
      |
      v
  nginx / caddy on the VPS  (:443 -> :8000)
      |
      v
  uvicorn 127.0.0.1:8000    FastAPI + SQLite (WAL)

No vincules uvicorn a 0.0.0.0 en esta topología. Vincular públicamente omite el proxy, expone el origen directamente y hace que FIN_TRUST_PROXY=1 sea inseguro (cualquier cliente podría entonces falsificar X-Forwarded-For y eludir el límite de velocidad). Docker es opcional y no es necesario.

Paso 1 — genera la sal del reportero una vez y consérvala.

sudo install -d -o failurenet -g failurenet /srv/failure-network/data
printf 'FIN_REPORTER_SALT=%s\n' "$(openssl rand -hex 16)" \
  | sudo tee /etc/failure-network.env > /dev/null
sudo chmod 600 /etc/failure-network.env

Generarla en línea en la línea de comandos crearía una nueva sal en cada reinicio, lo que restablece silenciosamente cada hash de reportero y cada conteo de reporteros únicos. Genera una vez, almacena una vez.

Paso 2 — comando de producción (lo que ejecuta la unidad de systemd):

set -a; . /etc/failure-network.env; set +a
export FIN_DATABASE_URL="sqlite+aiosqlite:////srv/failure-network/data/failure_network.db"
export FIN_PUBLIC_URL="https://failecho.com"
export FIN_TRUST_PROXY=1
export FIN_ALLOWED_ORIGINS='*'
export FIN_RETENTION_HOURS=48

/srv/failure-network/.venv/bin/python -m uvicorn app.main:app \
  --host 127.0.0.1 --port 8000 --workers 1 \
  --proxy-headers --forwarded-allow-ips '127.0.0.1' --no-server-header

Observa las cuatro barras en la URL de SQLite: sqlite+aiosqlite:/// más la ruta absoluta /srv/.... Tres barras la harían relativa al directorio de trabajo.

Paso 3 — proxy inverso. Caddy:

failures.example.com {
    reverse_proxy 127.0.0.1:8000
}

nginx:

location / {
    proxy_pass         http://127.0.0.1:8000;
    proxy_set_header   Host $host;
    proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_buffering    off;   # keeps /mcp responsive
}

Luego sudo cp deploy/failure-network.service /etc/systemd/system/ y sudo systemctl enable --now failure-network.

Lista de verificación de entorno

VariableValor de producciónPor qué
FIN_REPORTER_SALT32 caracteres hex de /etc/failure-network.envse establece una vez; rotarla desvincula los hashes de reportero existentes
FIN_DATABASE_URLsqlite+aiosqlite:////srv/failure-network/data/failure_network.dbruta absoluta, cuatro barras
FIN_TRUST_PROXY1 solo detrás del proxy anteriorde lo contrario, los clientes falsifican su propia identidad de límite de velocidad
FIN_ALLOWED_ORIGINS*, o https://yourdomainseparados por comas; * mantiene la API pública invocable desde el navegador
FIN_RETENTION_HOURS48las filas sin procesar más antiguas que esto se convierten en agregados por hora
FIN_DEMO_MODE0 en producción1 solo para una instancia de demostración
FIN_PUBLIC_URLhttps://failecho.comorigen canónico para enlaces, etiquetas y ejemplos
FIN_GITHUB_URLURL del repositorio, o sin establecerno se renderiza ningún enlace mientras no se establezca

Otras notas

  • Un solo worker. SQLite serializa las escrituras de todos modos, y el limitador de velocidad y el gestor de sesiones MCP son por proceso — dos workers significarían dos presupuestos de límite de velocidad independientes. Escala solo después de migrar a PostgreSQL.
  • MCP se sirve desde el mismo proceso en /mcp; responde con JSON plano, por lo que no se necesita ajuste de proxy específico de SSE más allá de deshabilitar el buffering.
  • Retención: deploy/failure-network-prune.timer, o la línea de cron anterior.
  • Copias de seguridad: copia data/ (incluyendo -wal/-shm) o ejecuta sqlite3 data/failure_network.db ".backup backup.db". No se necesita tiempo de inactividad.

La memoria residente está muy por debajo de 150 MB con el servidor MCP montado; SQLite se ejecuta en modo WAL con synchronous=NORMAL y un tiempo de espera de ocupado de 5 s, por lo que los lectores no son bloqueados por los escritores.

Migrar a PostgreSQL más adelante

Cada tipo de columna es portable, las marcas de tiempo son UTC ingenuo, no hay tipos específicos de SQLite ni índices de expresión. La migración es FIN_DATABASE_URL=postgresql+asyncpg://... más pip install asyncpg y una línea base de Alembic.


¿Está funcionando?

FailEcho publica los números que deciden si la idea se sostiene, en /v1/stats. Son deliberadamente poco halagadores.

MétricaQué responde
real_observations_24h¿está llegando algo real?
real_successes_24h¿tenemos denominadores, o solo quejas?
real_reporters_24h¿cuántos sistemas independientes?
known_hit_rate_24hcuando un agente pregunta, ¿sabe FailEcho algo?
recovery_outcome_ratio_24h¿dicen los agentes si la corrección funcionó?
cross_agent_help_24h¿usó un agente evidencia que no generó?

cross_agent_help_24h es la que importa. Cuenta una consulta solo cuando el llamante se identificó, se devolvió una recomendación y al menos un reportero detrás de esa recomendación era otra persona. Los llamantes anónimos y la evidencia de un solo reportero no se cuentan — subestimar el efecto es honesto, sobreestimarlo no lo es.

recovery_outcome_ratio_24h es la frágil. Informar de un fallo es automático; informar de si la corrección funcionó requiere que el agente regrese después. Sin esos informes, FailEcho es un contador de errores.

Hitos de lanzamiento

Marcadores de experimentos internos, no afirmaciones de marketing:

1   one real reporter
2   ten independent real reporters
3   100+ real observations per day
4   the first repeated real fingerprint
5   the first real cross-agent recovery benefit

El hito 5 es la hipótesis: un agente encuentra un fallo, consulta FailEcho, recibe evidencia generada por agentes no relacionados, cambia su comportamiento y se recupera. Todo lo anterior es plomería.


Limitaciones del MVP

Dichas claramente, porque fingir lo contrario haría la red menos útil:

  • Sin autenticación. Cualquiera puede informar cualquier cosa. El tope de evidencia por reportero y el limitador de velocidad aumentan el costo de envenenar las estadísticas; no lo hacen imposible, y una inundación distribuida desde muchas IPs aún podría pasar.
  • Sin puntuación de reputación. Los reporteros se cuentan, no se clasifican. Un reportero que ha acertado mil veces cuenta igual que uno nuevo.
  • El limitador de velocidad está en proceso y no es distribuido. Un worker de uvicorn, un presupuesto. Se restablece al reiniciar.
  • Sin detección sofisticada de anomalías. El estado es un umbral fijo sobre una proporción de fallos en dos ventanas fijas.
  • Los conteos de reporteros únicos en agregados son límites inferiores. Las identidades de reporteros no se retienen después de la poda, por lo que los buckets fusionados mantienen el máximo por bucket en lugar de un conteo distinto verdadero.
  • Sin ingesta de OpenTelemetry aún, sin SDK de TypeScript aún, sin pagos (x402 o de otro tipo). Todo es gratuito.
  • SQLite es una elección de etapa de prototipo. La retención mantiene el archivo pequeño, pero una red ocupada eventualmente querrá PostgreSQL (un cambio de URL más asyncpg).
  • Las acciones de recuperación son cadenas de forma libre, por lo que refresh_schema y refreshSchema se contarían por separado si los agentes no coinciden en la ortografía (la entrada se convierte a minúsculas y se normalizan los espacios, lo que maneja los casos comunes).

Documentación de lanzamiento

  • docs/marketing.md — mensajes aprobados, publicaciones de lanzamiento y las afirmaciones que nunca deben hacerse
  • docs/launch-plan.md — secuencia de distribución, métricas de experimento y los hitos que deciden si esto funciona
  • docs/search-console.md — lista de verificación de indexación para Google Search Console y Bing, y lo que realmente mueve la búsqueda de marca

Contacto

Licencia

MIT.