Búsqueda: encontrar la conversación, el patch y la decisión

Por: Artiko
buzznostrbusquedanip-50clipostgres

Búsqueda: encontrar la conversación, el patch y la decisión

En la mayoría de las herramientas, buscar significa elegir primero dónde buscar. El chat tiene su buscador, el forge tiene el suyo, el runner de CI tiene otro y la decisión de aprobar el despliegue no está en ninguno: está en un hilo de Slack que alguien archivó.

Buzz elimina esa elección por construcción. El README lo dice sin rodeos entre las cosas que uno hace en el producto:

«Search the conversation, the patch, the workflow run, and the approval in one place — because they’re all the same kind of event.»

Esa frase describe el modelo de datos, y el modelo de datos es cierto: un mensaje de canal, un diff pegado en un hilo, la definición YAML de un workflow, un issue, un pull request y la nota con la que alguien concedió una aprobación son todos eventos Nostr firmados que viven en la misma tabla events de la misma comunidad, alcanzables desde el mismo relay con la misma identidad.

Lo que conviene separar desde el primer minuto es qué se alcanza por texto libre y qué se alcanza por filtro. El índice de texto completo que se despliega hoy no cubre todos esos kinds: cubre la conversación. Los patches, issues, PRs y workflows se recuperan por filtro NIP-01 y por sus listados dedicados, no escribiendo una palabra en un buscador. Este capítulo explica exactamente dónde está esa línea.

Un solo índice, porque hay un solo registro

El índice no es un servicio aparte. Es una columna generada de Postgres sobre la tabla de eventos, con un índice GIN (search_tsv) como camino de acceso. En una instalación nueva la migración 0008_fresh_install_search_allowlist.sql la deja con esta forma — una lista blanca positiva de kinds indexables:

search_tsv TSVECTOR GENERATED ALWAYS AS (
    CASE WHEN kind IN (0, 9, 40002, 45001, 45003)
         THEN to_tsvector('simple', content)
         ELSE NULL::tsvector
    END
) STORED

Dos matices que evitan confusión al leer el repositorio:

  • El schema/schema.sql y las migraciones 0001 y 0005 muestran la forma antigua: una lista negra de kinds sensibles con el resto indexado. La migración 0008 solo reescribe la columna si la tabla events está vacía, así que un relay que ya tenía datos conserva la expresión vieja hasta que su operador ejecute a mano scripts/maintenance/nip_rs_search_allowlist.sql.
  • La migración 0014 envuelve después la expresión que haya con una exclusión adicional del kind 30350 (push lease). No cambia nada más.

Todo lo que sigue describe la lista blanca, que es lo que obtiene un relay levantado hoy desde cero. Esa definición tiene tres consecuencias directas que se notan al usar el producto:

  1. No hay ventana de indexación. Como la columna es GENERATED ALWAYS, cada escritura de fila es la actualización del índice. No hay indexador fuera de banda, ni cola, ni job de reindexado, ni ese lapso incómodo en el que un mensaje ya existe pero todavía no aparece al buscarlo.
  2. Nadie puede falsificar su propia indexación. El cliente firma el content; el tsvector lo deriva la base de datos. No hay forma de publicar un evento cuyo texto indexado difiera del texto firmado.
  3. La comunidad es una frontera de tipo, no una convención. Toda consulta de búsqueda lleva un community_id obligatorio y lo emite como primer predicado del WHERE. No existe un camino de código que lo omita.
flowchart TD
    A["Escribes un mensaje, pegas un diff,<br/>guardas un workflow o apruebas un paso"] --> B["Evento Nostr firmado con tu clave"]
    B --> C["INSERT en la tabla events"]
    C --> D{"Kind dentro de la lista blanca<br/>0, 9, 40002, 45001, 45003"}
    D -- "si" --> E["search_tsv se calcula en el mismo INSERT<br/>columna GENERATED ALWAYS"]
    D -- "no" --> N["search_tsv queda a NULL<br/>inalcanzable por texto"]
    E --> F["Indice GIN sobre search_tsv"]
    F --> G["Busqueda por texto libre"]
    N --> H["Se recupera por filtro NIP-01<br/>kinds, tags, autores"]

Qué entra al índice y qué no

La lista blanca del CASE WHEN no es una preferencia de la interfaz: lo que queda fuera jamás entra al índice, porque un tsvector nulo nunca casa con el operador @@. Ningún cliente, ningún agente y ninguna consulta puede alcanzarlo por texto.

Entra exactamente esto:

KindQué es
0Metadatos de perfil (NIP-01). Es lo que hace posible resolver un autor por nombre
9Mensaje de canal
40002Mensaje de stream v2, con contenido enriquecido
45001Post raíz de foro
45003Comentario de foro

Todo lo demás queda fuera, y conviene tener presentes dos grupos distintos:

Fuera por privacidad. Aquí la exclusión es el objetivo, no un efecto colateral. Son los kinds que ya estaban en la lista negra histórica y que la lista blanca sigue dejando fuera: 1059 (gift wrap NIP-17, el sobre de los mensajes directos cifrados), 30300 (recordatorio cifrado, de lectura solo del autor), 30350 (push lease), 30622 (snapshot por espectador de los DM ocultos), 44100 y 44101 (notificaciones p-gated de miembro añadido y eliminado) y 44200 (métrica de turno de agente, cifrada al dueño).

Fuera por alcance del índice. Estos existen, están firmados, viven en la misma tabla y los puedes leer perfectamente — simplemente no se alcanzan escribiendo palabras. Son los diffs pegados en canal (40008), los patches NIP-34 (1617), los pull requests (1618), los issues (1621), las definiciones de workflow (30620) y las notas largas NIP-23 (30023). Para ellos existen listados dedicados y filtros por kind, tag y autor, que se cubren más abajo en «Otros comandos que encuentran cosas sin pasar por el índice».

La consecuencia práctica más importante del primer grupo: tus mensajes directos cifrados no son buscables, ni por ti ni por nadie. Es una decisión de arquitectura, no una funcionalidad pendiente. El capítulo Mensajes directos y privacidad explica por qué el transporte está construido así — y por qué el DM de la aplicación de escritorio, que no es un gift wrap sino un canal de tipo dm con mensajes kind 9, sí entra al índice.

La búsqueda nunca es la frontera de acceso

Esta es la regla que conviene interiorizar antes de confiar en cualquier resultado: el motor de texto completo devuelve candidatos, no respuestas.

El relay toma cada acierto, vuelve a leer el evento canónico desde la base de datos con el par (community_id, event_id) y ejecuta el predicado de acceso sobre el evento completo antes de entregártelo. Ese predicado comprueba tres cosas: que el evento case realmente con el filtro NIP-01 que enviaste, que su canal esté dentro de los canales a los que tienes acceso, y que tú estés autorizado a leerlo por las reglas de visibilidad del kind.

sequenceDiagram
    participant C as Cliente
    participant R as Relay
    participant S as Indice FTS
    participant D as Tabla events
    C->>R: REQ con search, kinds, authors, since
    R->>S: Consulta acotada a tu comunidad
    S-->>R: Candidatos ordenados por relevancia
    R->>D: Relee cada evento canonico por id
    D-->>R: Evento completo firmado
    R->>R: Filtro NIP-01 + acceso al canal + visibilidad del kind
    R-->>C: Solo los aciertos que ya podias leer, luego EOSE

Traducido: la búsqueda no puede ensancharte la visibilidad. Nunca vas a ver en resultados un mensaje de un canal privado al que no perteneces, ni el contenido de un evento reservado a su autor. Y al revés: si un compañero te pasa un enlace a un resultado suyo y tú no puedes abrirlo, no es un fallo, es la membresía de canal haciendo su trabajo.

Buscar desde el CLI

El comando central es buzz messages search. Estos son sus flags, tal como los declara el CLI, sin nada más:

FlagTipoObligatorioNota
--querytextoopcional si das --authorEl texto que va al índice FTS
--authorhex de 64, npub1… o nombre visibleopcional si das --querySe resuelve a una pubkey antes de consultar
--sincetimestamp Unix en segundosnoCota inferior de created_at
--limitenteronoPor defecto 20, tope 100

No hay más. No existe --until, ni --channel, ni --kind en este subcomando: si necesitas acotar por canal o por tipo de evento, tendrás que usar un cliente Nostr contra el relay, como se ve más abajo.

# Texto libre en todo el workspace
buzz messages search --query "checkout"

# Todo lo que escribió alguien desde una fecha, sin filtrar por texto
buzz messages search --author npub1... --since 1783497600

# Las tres cosas a la vez
buzz messages search --author Aaron --query "checkout" --limit 20

Si no pasas ni --query ni --author, el comando falla antes de tocar la red con at least one of --query or --author is required, que sale como user_error en stderr y termina con código 1.

Qué eventos cubre este comando

buzz messages search consulta exactamente cuatro kinds:

  • 9 — mensaje de canal
  • 40002 — mensaje de stream v2 con contenido enriquecido
  • 45001 — post raíz de foro
  • 45003 — comentario de foro

Es decir: cubre la conversación, incluidos los foros — que es justo la parte indexable del registro, menos el kind 0 que el propio comando usa por dentro para resolver --author por nombre.

No cubre los diffs publicados como kind 40008, ni los patches NIP-34 kind 1617, ni los pull requests, issues, definiciones de workflow o notas largas. Y no es una limitación del CLI que puedas rodear bajando a un cliente Nostr: esos kinds tampoco están en el índice de texto completo, así que un REQ con search sobre ellos devuelve vacío. Se alcanzan por filtro, y eso es lo que hacen los comandos de la sección siguiente.

Cómo se resuelve --author

El valor de --author se interpreta en este orden de precedencia:

  1. 64 caracteres hexadecimales → se valida y se usa tal cual, en minúsculas.
  2. Empieza por npub1 → se decodifica a hex. Si el bech32 es inválido, error de uso: invalid npub: ….
  3. Cualquier otra cosa → se trata como nombre visible y se resuelve con una búsqueda NIP-50 sobre perfiles kind:0, exigiendo coincidencia exacta e insensible a mayúsculas contra display_name o name.

El tercer caso tiene dos finales posibles que conviene conocer:

# Nadie se llama así
buzz messages search --author Fulanito --query deploy
# → no user found with name 'Fulanito' — pass a hex pubkey or npub instead

# Varias personas comparten el nombre
buzz messages search --author Alex --query deploy
# → name 'Alex' is ambiguous — matches: Alex (a1b2…), Alex (c3d4…),
#   … and 3 more. Pass a pubkey instead

La ambigüedad es un error, no una mezcla silenciosa de autores. El listado de candidatos se corta en cinco y añade el contador del resto. Es deliberado: más vale un error legible que un informe construido sobre dos personas distintas.

Que la resolución por nombre funcione sobre perfiles no es magia añadida: el contenido de un kind:0 es JSON, y el analizador de Postgres tokeniza a través de la puntuación, así que wonderland, alice y [email protected] encuentran el mismo perfil sin ningún aplanamiento previo.

Orden de los resultados

Depende de si diste texto o no:

  • Con --query: el orden es el que devuelve el índice, es decir relevancia primero, y ante empate el evento más reciente.
  • Sin --query, solo con --author: no hay relevancia que ordenar, así que el CLI reordena localmente del más nuevo al más viejo, igual que buzz messages get.

Toda la salida es JSON en stdout. Con --format compact los eventos se reducen a los campos justos para escanear rápido — útil cuando el consumidor es un agente o un script. El contrato completo de entrada y salida del CLI está en buzz-cli: el workspace desde la terminal.

Otros comandos que encuentran cosas sin pasar por el índice

No todo lo que se parece a buscar es búsqueda de texto completo. Conviene no confundirlos:

ComandoQué hace realmente
buzz channels search --query <texto>Trae los eventos de metadatos de canal y filtra por subcadena del nombre en el cliente. Acepta --exact y --include-archived
buzz users get --name <nombre>Busca perfiles por nombre; con --owner me acota a tus agentes
buzz patches list / buzz pr list / buzz issues listListados NIP-34 por repositorio, autor y etiqueta. Sin texto libre
buzz notes ls --tag <tag>Lista notas largas propias o de otro autor por etiqueta
buzz feed get --types mentionsTu bandeja de actividad, no el índice histórico

Los tres del medio pertenecen a Git dentro de Buzz; el último se explicó en Hilos, reacciones y actividad.

Buscar desde el escritorio

La barra superior del cliente de escritorio es un buscador tipo typeahead: se dispara a partir de dos caracteres, con 300 ms de rebote, y agrupa los resultados en secciones fijas, en este orden: Channels, Direct messages, People, Agents, Most relevant y Actions. Los canales y las personas se resuelven localmente; la sección de mensajes es la que va al índice.

Esa consulta usa un modo de coincidencia distinto al del CLI: prefijo. Los tokens ya completados se buscan exactos y solo al último se le añade el comodín de prefijo. Escribir project pro encuentra project plan pero no projectile plan: project se exige entero y solo pro completa. Es el comportamiento que uno espera mientras teclea, y no altera la búsqueda normal por palabras del resto de superficies.

Operadores al estilo Slack

El campo acepta cuatro operadores, que el parser extrae del texto y convierte en filtros reales antes de consultar:

OperadorValor aceptadoEfecto
from:pubkey hex, npub, o @nombreFiltra por autor
in:UUID de canal o #nombreAcota a ese canal
after:YYYY-MM-DDCota inferior, inclusiva, al inicio local de ese día
before:YYYY-MM-DDCota superior exclusiva del día nombrado

Ejemplo: deploy fallido from:@Ana in:#infra after:2026-07-01.

Detalles del comportamiento que evitan sorpresas:

  • El operador debe empezar en frontera de token. built-in:react o una URL con .../in:foo no se interpretan como operadores.
  • Si el mismo operador aparece dos veces, gana el último.
  • Una fecha que no sea YYYY-MM-DD no se descarta: se queda en el texto y participa en la búsqueda. Por eso after:ayer busca literalmente esa palabra.
  • La puntuación final se limpia: in:general, resuelve a general.
  • Los nombres con espacios no funcionan todavía: from:@Ana María solo captura el primer token. Usa la pubkey.
  • Si un from: o un in: no se puede resolver, la búsqueda no se ejecuta y no devuelve nada. Nunca se ensancha en silencio a todo el workspace, que sería la forma más rápida de citar el mensaje equivocado.

Buscar dentro del canal abierto

Dentro de un canal existe una barra de búsqueda local, con el atajo estándar de la plataforma para findCmd+F en macOS, Ctrl+F en el resto — y el marcador de posición Find in channel, con navegación a la coincidencia anterior y siguiente.

Su comportamiento es híbrido y por eso resulta rápida: primero marca las coincidencias por subcadena sobre los mensajes ya cargados en pantalla, de forma instantánea; en paralelo consulta el índice del relay acotado a ese canal, y añade los aciertos que caen fuera de la ventana cargada. Es la diferencia entre «lo que tengo delante» y «todo lo que se dijo en este canal».

Buscar desde un cliente Nostr

Buzz implementa NIP-50. Una búsqueda es un REQ normal que incluye el campo search; el relay responde con los aciertos ordenados por relevancia y cierra con EOSE. Es importante entender que no se registra como suscripción persistente: es una consulta de una sola tanda, no un flujo que siga llegando.

Con nak, tal como está documentado en el repositorio:

# Buscar mensajes de un canal
nak req -k 9 --tag "h=<channel-uuid>" --search "search query" -l 20 \
  --auth --sec <privkey> ws://localhost:3000

La razón para bajar a este nivel no es alcanzar más kinds — el índice es el mismo que usa el CLI y contiene lo mismo — sino combinar la búsqueda de texto con filtros que el CLI no expone: acotar por canal con #h, por autor, o poner una cota superior de tiempo con until.

# Texto libre acotado a un canal y a una ventana con los dos extremos
nak req -k 9 --tag "h=<channel-uuid>" --search "connection pool" \
  --since 1783497600 --until 1785312000 -l 50 \
  --auth --sec <privkey> ws://localhost:3000

# Solo lo que escribio una persona, dentro de un canal
nak req -k 9 -k 40002 --tag "h=<channel-uuid>" --author <pubkey-hex> \
  --search "rollback" -l 50 --auth --sec <privkey> ws://localhost:3000

Lo que no va a funcionar, y conviene no perder tiempo intentándolo: añadir --search a un filtro de kind 40008, 1617, 1618, 1621, 30620 o 30023. Esos kinds tienen search_tsv a NULL, así que el operador @@ nunca casa y la respuesta llega vacía sin ningún error. Para ellos el camino es el filtro puro, sin search:

# Diffs pegados en un canal, kind 40008 — se filtra, no se busca
nak req -k 40008 --tag "h=<channel-uuid>" -l 50 \
  --auth --sec <privkey> ws://localhost:3000

# Patches NIP-34 de un repo, kind 1617 — o mejor, buzz patches list
nak req -k 1617 --tag "a=30617:<owner-hex>:<repo-id>" -l 50 \
  --auth --sec <privkey> ws://localhost:3000

Y para el caso normal, los listados del CLI son más cómodos y hacen exactamente eso por debajo: buzz patches list, buzz pr list, buzz issues list, buzz workflows list y buzz notes ls.

Las reglas de acceso del relay siguen aplicando sin excepción sobre cada acierto, y las reglas generales de suscripción del capítulo Otros clientes: Nostr de terceros y móvil también: si buscas en un canal, incluye el tag #h.

Tres estrategias que funcionan

Reconstruir un incidente

El objetivo no es encontrar un mensaje: es reconstruir la secuencia. Un orden que funciona bien:

# 1. Fija la ventana temporal. Todo lo demás cuelga de aquí.
INICIO=$(date -d '2026-07-14 00:00' +%s)

# 2. El síntoma, por texto, en todo el workspace.
buzz messages search --query "504 checkout" --since "$INICIO" --limit 50

# 3. Del acierto más antiguo, abre el hilo completo: ahí suele estar la
#    discusión que no menciona el término que buscaste.
buzz messages thread --channel <uuid> --event <event-id>

# 4. La ventana cruda del canal alrededor de esa hora, sin filtro de texto.
buzz messages get --channel <uuid> --since "$INICIO" --limit 200

# 5. El cambio de código que lo causó o lo arregló.
buzz patches list --repo-owner <hex> --repo-id <repo> --limit 50
flowchart LR
    A["Sintoma en texto<br/>messages search"] --> B["Hilo completo<br/>messages thread"]
    B --> C["Ventana del canal<br/>messages get --since"]
    C --> D["Cambios de codigo<br/>patches list / pr list"]
    D --> E["Decision y aprobacion<br/>mensajes del canal donde se acordo"]

El paso 3 es el que la gente se salta y el que más rinde: la búsqueda por texto te da la puerta de entrada, el hilo te da el contexto. Casi nunca están en el mismo mensaje.

Encontrar quién tocó algo

Aquí --author sin --query es la herramienta correcta, no un uso degenerado:

# Todo lo que publicó esa persona desde el 1 de julio, más nuevo primero
buzz messages search --author npub1... --since 1782950400 --limit 100

Si no sabes la pubkey, empieza por el nombre y acepta que puede fallar por ambigüedad — y cuando falle, usa el listado de candidatos que te devuelve el propio error para elegir la pubkey correcta. Para el lado del código, buzz patches list --author <hex> y buzz pr list --author <hex> cierran el mismo círculo.

Una nota importante: los agentes tienen su propia clave y su propia autoría. Si un agente escribió el mensaje, aparecerá bajo su pubkey, no bajo la de su dueño. Eso está desarrollado en Agentes como compañeros de equipo.

Citar con recibos

La costumbre más útil que puedes adoptar: cuando respondas «esto ya se decidió», pega el resultado, no el recuerdo. Cada acierto trae id, pubkey y created_at; con eso, cualquiera puede releer el hilo original y comprobarlo por su cuenta.

buzz messages search --query "migrar a Postgres" --limit 5 --format compact

Es exactamente lo que el README describe para los agentes: buscan meses de historia y publican los hilos, no impresiones. La diferencia entre una discusión que se repite cada trimestre y una que se cierra suele ser un identificador de evento.

Límites del índice

Ninguno de estos es un fallo. Todos son comportamientos verificables del sistema, y todos han hecho que alguien concluya lo contrario de lo que decían los datos.

El analizador es simple: no hay lematización ni palabras vacías. Esto es lo más importante de toda la sección. desplegar no encuentra despliegues, deploy no encuentra deploys. Cada forma es un lexema distinto. Busca la raíz más corta y compensa con el modo prefijo del escritorio cuando puedas.

El texto de búsqueda se corta a 4096 caracteres. De sobra para cualquier consulta humana; relevante solo si un script arma la consulta pegando contenido.

Cada consulta escanea como máximo 1000 candidatos, en diez páginas de cien, y de esos candidatos el relay descarta los que no pasan el filtro completo o el control de acceso. Por eso es normal recibir menos resultados que el limit que pediste: el límite acota lo que se te puede entregar, no promete un número. Una búsqueda muy genérica en un workspace grande puede quedarse corta sin que nada haya fallado. La solución no es subir el límite: es acotar con autor, canal o fecha.

Una página de resultados no supera los 500, y por defecto trae 100. El máximo de resultados históricos por filtro que el relay anuncia en su documento NIP-11 (limitation.max_limit) es 1000, y ahí es donde se clampa cualquier limit que pidas de más.

Los DM cifrados no se buscan. El gift wrap NIP-17 tiene search_tsv a NULL. Los DM de la aplicación de escritorio, en cambio, son un canal de tipo dm con mensajes kind 9, y sí aparecen en tus resultados. Es la asimetría que más veces sorprende: repásala en el capítulo 6.

El texto libre solo alcanza la conversación. Diffs, patches, PRs, issues, workflows y notas largas no están en el índice. Añadirles search no da error: da cero resultados. Úsalos con sus listados o con un filtro por kind y tag.

No puedes mezclar filtros de búsqueda y filtros normales en la misma petición. Si un cliente envía a la vez un filtro con search y otro sin él, la petición se rechaza con mixed search and non-search filters not supported. Manda dos consultas.

Una búsqueda no es una suscripción. Los resultados son una foto del momento; si quieres seguir el canal en vivo, suscríbete al canal.

El CLI no tiene cota superior de tiempo. Hay --since pero no --until. Para acotar por ambos extremos, usa el escritorio con after: y before:, o un REQ con since y until.

Los eventos efímeros no existen para la búsqueda. Los kinds del rango 20000 a 29999 — presencia, «está escribiendo», telemetría del observador de agentes — no se almacenan, así que tampoco se indexan ni se auditan.

El índice está acotado a tu comunidad. Si tu equipo usa más de un relay, cada uno es un mundo aislado de canales, miembros y búsqueda. Tu clave es portable; tu historial no. Lo relativo a cómo se despliega y se aísla cada comunidad pertenece al curso de administrador: curso de administrador de Buzz.

Resumen

  • El índice de búsqueda es una columna generada de Postgres sobre la tabla de eventos, con índice GIN: cada escritura de evento es la actualización del índice, sin ventana de indexación ni indexador aparte.
  • En una instalación nueva ese índice es una lista blanca: solo se indexan los kinds 0, 9, 40002, 45001 y 45003. Un relay que ya tenía datos conserva la expresión antigua hasta que su operador ejecute el script de mantenimiento.
  • Mensajes, diffs, patches, issues, PRs, workflows y notas comparten el mismo registro y el mismo relay, pero no el mismo espacio de texto libre: la búsqueda alcanza la conversación; el resto se recupera por filtro y por sus listados dedicados.
  • Los gift wraps NIP-17 y los demás kinds sensibles quedan fuera del índice a nivel de almacenamiento. Los DM de la aplicación, en cambio, son canales con mensajes kind 9 y sí se buscan.
  • La búsqueda nunca es la frontera de acceso: el relay relee cada acierto y revalida filtro, membresía de canal y visibilidad antes de entregarlo.
  • buzz messages search acepta exactamente --query, --author, --since y --limit, con 20 por defecto y 100 de tope, y cubre los kinds 9, 40002, 45001 y 45003.
  • --author acepta hex, npub o nombre visible; el nombre exige coincidencia exacta y la ambigüedad es un error con lista de candidatos.
  • Con --query el orden es por relevancia; solo con --author, del más nuevo al más viejo.
  • El escritorio añade modo prefijo para escritura en vivo y los operadores from:, in:, after: y before:, con fechas YYYY-MM-DD; un operador sin resolver no ensancha la búsqueda, la cancela.
  • Dentro de un canal, Cmd+F o Ctrl+F combina coincidencias locales instantáneas con el índice del relay acotado a ese canal.
  • Desde un cliente Nostr, un REQ NIP-50 de una sola tanda no amplía los kinds indexados; lo que aporta es combinar el texto con #h, authors, since y until en el mismo filtro.
  • El analizador es simple: sin lematización. Busca raíces cortas y no esperes que un plural encuentre su singular.
  • Recibir menos aciertos que el limit es normal: el escaneo está acotado a 1000 candidatos y el filtrado posterior descarta una parte impredecible. El tope anunciado por NIP-11 es 1000, y una página nunca supera 500.

Siguiente: Agentes como compañeros de equipo