Servidor de jerarquía web
El humano para el que trabaja un agente a menudo está en otro dispositivo y no puede ver la terminal de los subagentes. Esto convierte todo el árbol de procesos en una página en vivo que abren una vez y observan: un gráfico clicable en la parte superior, el registro completo del agente seleccionado abajo.
El contexto de instalación de esta habilidad - el directorio
fleet where web-hierarchy-server imprime - contiene los dos archivos de esta instalación:
CONTEXT.md nombra al agente, su puerto y su URL, y config.json contiene todos
los valores que los scripts leen. config.example.json junto a este archivo documenta
cada clave. Los scripts encuentran config.json preguntando a ese mismo comando, o leen
el archivo que WEB_HIERARCHY_CONFIG nombra cuando está configurado; una ruta relativa dentro de él
se resuelve contra su propio directorio.
Ejecute las comprobaciones y scripts por ruta:
"${CLAUDE_SKILL_DIR}/installation/check" # ¿hay un servidor levantado para este directorio de configuración? ¿está actualizado?
node "${CLAUDE_SKILL_DIR}/serve.mjs" --help # la configuración, la raíz de transcripción y el puerto que usaría
La única regla fija: UN servidor por directorio de configuración
Un directorio de configuración (CLAUDE_CONFIG_DIR) es la raíz de transcripción de un agente, y exactamente
un servidor lo sirve. Un segundo servidor en el mismo puerto es el fallo que esta regla
existe para prevenir: cualquier proceso que gane el enlace sirve la página, el otro
muere con EADDRINUSE, y desde el lado del visor la página se ve bien de cualquier
manera. Antes de iniciar uno, establezca que ningún servidor ya está activo para ese directorio
de configuración (installation/check lo responde y nunca inicia nada).
La forma duradera es una unidad de servicio (installation/web-hierarchy-server.service.example):
inicio con la máquina, Restart=always, activo desde el arranque antes de que se delegue cualquier trabajo, y
que sobrevive a reinicios. Instalarlo requiere privilegios de root, que es un paso del humano. En macOS
es un LaunchAgent en el dominio gui del usuario
(installation/web-hierarchy-server.plist.example), instalado sin sudo. Donde
nadie lo ha ejecutado, el servidor se ejecuta como un proceso separado iniciado una vez
(setsid nohup ... & en Linux, nohup ... & en macOS, que no tiene setsid, por lo que
sobrevive a la sesión que lo lanzó) - esa forma
funciona y no sobrevive a un reinicio, que es exactamente lo que installation/check
dice en palabras en lugar de reportar un verde.
Las ediciones a serve.mjs o format.mjs son LATENTES por diseño. node las carga
una vez al iniciar el proceso, por lo que un cambio aplicado llega a una página en ejecución solo en el
siguiente reinicio. Eso es esperado; no es una razón para reiniciar un servidor que un
visor está viendo, y ES la razón por la que un informe de "mi arreglo no funcionó" suele
deberse a un proceso en ejecución que mantiene el código antiguo.
El árbol se SIRVE; los registros se OBTIENEN
Esta es la forma de todo el sistema, y es una regla de tamaño, no de estilo.
El ÁRBOL de una sesión cuesta aproximadamente un kilobyte por agente, independientemente de lo que ese agente haya hecho. Sus
REGISTROS no tienen límite, y renderizar uno arrastra sus imágenes consigo. Poner el registro
de cada agente en cada instantánea hace que GET / para una sesión de 228 agentes sea 358 MB,
110 MB de los cuales son datos de imagen en base64, reenviados en cada sondeo de 10 s. Nada está roto, y
la página no se puede abrir.
Por lo tanto, la carga útil se divide, y cada ruta lleva un tipo de cosa:
| Ruta | Lleva | Costes |
|---|---|---|
GET / | todo el árbol, el registro de la sesión RAÍZ y las imágenes de ese contenido en línea como URI de datos | un documento, completo sin un servidor |
GET /state | todo el árbol y SIN registros (?logs=<id>,<id> nombra excepciones); ?view=active reduce el GRÁFICO a la vista activa | lo que paga el sondeo de 10 s, cada tick, para siempre |
GET /agent/<id> | exactamente el registro renderizado de ese agente | una obtención, cuando el visor selecciona un bloque |
GET /agent/<id>?since=<n> | solo eventos desde el índice n | un array vacío para un agente silencioso |
GET /img/<hash> | una imagen por hash de contenido, immutable | una vez por imagen por visor, luego en caché |
Dos consecuencias que vale la pena conocer antes de cambiar algo aquí:
sinceindexa el propiocountdel árbol. Una fila del árbol lleva el recuento real de eventos del agente, incluso si su registro no se renderizó, y ese número es lo que un cliente devuelve comosince. Ambos salen del mismo análisis de transcripción, y deben hacerlo: derivar uno de ellos en otro lugar y un cliente silenciosamente salta o repite eventos, sin nada que ver excepto un registro sutilmente incorrecto.GET /img/<hash>está direccionado por contenido, por lo que una respuesta se almacena en caché para siempre. Por eso un hash desconocido es un 404 y nunca un 200 vacío - un 200 vacío es una imagen permanentemente rota para ese visor, y ninguna recarga lo arregla.
Nada se vuelve rojo si se pierde esta división. Ponga todos los registros en GET /state, o
imágenes en línea en cada instantánea, y el árbol es correcto, las etiquetas son correctas, la página se renderiza, y tiene cientos de megabytes. El TAMAÑO es la propiedad protegida; las pruebas de tamaño
en serve.test.mjs (6-9) son lo único que puede ver su regresión, y cada una es un
PAR - ausente aquí, presente allí - porque "X está ausente" es verdadero por todas las razones equivocadas.
Lo que una página GUARDADA tiene, y lo que no
GET / es un documento HTML autónomo: CSS y JS en línea, sin activos externos,
y una instantánea incorporada que se renderiza sin ningún servidor. Lo que está horneado es todo el árbol y el registro de la sesión raíz, con las imágenes de ese contenido como URI de datos.
No horneado: el registro de todos los demás agentes. Esos no están en el documento; llegan desde /agent/<id> al seleccionar. Por lo tanto, una página guardada en disco, o una cuyo servidor ha muerto, muestra el gráfico completo y el registro de la sesión raíz, y no muestra nada cuando se hace clic en un bloque de subagente. Las imágenes dentro de esos registros siguen la misma historia: se renderizan desde /img/<hash> y no hay un servidor detrás de un archivo guardado.
Para un registro congelado de TODOS los registros en un solo archivo, use build.mjs - la horneada estática incorpora en línea el registro de cada agente y cada imagen, porque un recuerdo no tiene nada que obtener. Ese es el único llamante que pide a snapshot() todos los registros (logsFor: null).
Cómo sigue las sesiones
Un servidor tiene una larga vida; una sesión de Claude no: finalizar una sesión, una actualización y un reinicio de la unidad crean un nuevo id de sesión. Por lo tanto, el servidor vuelve a elegir la sesión más recientemente activa en cada instantánea en lugar de resolver una al inicio, y sirve una página de "esperando una sesión..." mientras no exista ninguna - el estado normal al arrancar. "Más recientemente activa" incluye la transcripción RAÍZ, no solo los archivos de subagente: una sesión recién iniciada que aún no ha delegado se sigue inmediatamente y se renderiza como un árbol solo raíz, y el árbol de procesos crece desde su primera delegación. No es necesario reiniciar nada cuando comienza una nueva sesión; la página detecta el cambio de id y reconstruye sus registros.
--session=<id> fija una sesión en su lugar (una vista congelada para depuración).
Se escanea cada slug de proyecto bajo el directorio de configuración, no solo uno. La transcripción de una sesión se ubica bajo el slug de proyecto de su CWD, por lo que una sesión que trabaja en un worktree de git escribe bajo el slug del WORKTREE; fijar un slug hace que cada sesión de worktree sea invisible mientras la página aún se renderiza perfectamente.
Qué agente dice estar mostrando una página
El bloque raíz está etiquetado con el miembro del fleet al que pertenece el directorio de configuración, resuelto a través de config.json's fleet.by_config_dir. La clave se deriva, no es el nombre base sin procesar, y la derivación es la parte que vale la pena leer:
fleetKey()deformat.mjstoma el nombre base del directorio de configuración, EXCEPTO cuando ese nombre base es el literal.claude, donde toma el nombre base del directorio PADRE en su lugar. Un fleet que da a cada agente un directorio de inicio coloca cada directorio de configuración en<home>/<agent>/.claude, por lo que si se usara el nombre base, todos ellos chocarían en una sola entrada y la página de cada agente se etiquetaría con el agente que nombra esa entrada.
Ese fallo es silencioso: la página se renderiza completamente y dice el nombre equivocado, y un visor con dos páginas abiertas no puede distinguirlas y no sabe que no puede. Una entrada literal .claude se honra como respaldo, por lo que una instalación de un solo agente con clave .claude mantiene su etiqueta; no puede distinguir dos agentes y no debe usarse donde exista más de uno. La misma clave derivada elige el puerto predeterminado de config.json's servers, por la misma razón: una coincidencia de nombre base daría a cada agente el puerto de la primera entrada.
Archivos
serve.mjs- el servidor en vivo:GET /,GET /state,GET /agent/<id>yGET /img/<hash>(vea la tabla de rutas arriba). La herramienta principal.--port=N,--host=<addr>,--cap=KB,--session=<id>, cada uno con valor predeterminado deconfig.json;--helpimprime el uso, la ruta de configuración resuelta, la raíz de transcripción y el puerto predeterminado, y no enlaza nada.--port=0toma un puerto efímero y el banner imprime el que realmente obtuvo.format.mjs- el formateador compartido: análisis de transcripción, diseño del árbol de procesos, HTML por evento, el CSS del tema, y el único lugar donde se leen valores locales (cfg()sobre elconfig.jsonquefleet wherenombra, o sobreWEB_HIERARCHY_CONFIGcuando está configurado).build.mjs- instantánea ESTÁTICA opcional (node build.mjs <out.html>): hornea el estado actual en un archivo autónomo para un registro congelado y compartible. Úselo para un recuerdo, no para la vista en vivo.identities.json+avatars/- el registro de nombres para mostrar, usado a menos queidentities.filenombre otro.serve.test.mjs+fixtures/- la prueba: inicia un servidor real en un puerto efímero contra un árbol de transcripción sintético, afirma el árbol, conduce cada ruta (árbol sin registros, el registro de un agente y de nadie más,?sinceenviando solo lo que es nuevo, una imagen servida byte por byte por hash), y luego ELIMINA la transcripción de un subagente y requiere que el árbol Y la ruta por agente lo pierdan. Su encabezado establece por qué cada prueba está allí, de qué par es mitad cada prueba de tamaño, y por qué el control negativo es la parte que soporta la carga. La prueba 11 requiere queGET /state?view=activeelimine el bloque detenido que la vista predeterminada dibuja.view.test.mjs- la regla "solo activos" por sí sola (filterView()sobre un árbol sintético): bloques vivos, los bloques encima de ellos y la raíz, con los hermanos e hijos detenidos junto a ellos eliminados, y "todos" devolviendo cada fila sin cambios.format.test.mjs- cómo se encuentra y lee la configuración: a través defleet wherecuandoWEB_HIERARCHY_CONFIGno está configurado (un fleet falso seleccionado porFLEET_BIN), la variable ganando, un fleet fallido retransmitido en lugar de adivinado, rutas relativas, el registro predeterminado ypackages.dir_pattern.installation/- la plantilla de unidad, el procedimiento de instalación ycheck.
Lo que muestra la página
Se lee como la consola de Claude Code en el tema oscuro para daltonismo (dark-daltonized) - la paleta tomada textualmente del paquete de Claude Code (fondo negro, texto monoespaciado blanco, viñetas de herramientas en azul éxito #3399ff, errores
#ff6666, líneas de resultado atenuadas con expansores "... +N líneas", marcadores de pensamiento,
mensajes de tarea y usuario enmarcados como la entrada de la consola).
- Gráfico de árbol de procesos. La sesión raíz es el nodo superior; cada agente sin padre
cuelga de él. Azul = vivo (con un arco giratorio junto al rango de tiempo;
estático bajo
prefers-reduced-motion), gris = detenido. Haga clic en un bloque para mostrar el registro completo de ese agente abajo. - Un interruptor "todos / solo activos" encima del gráfico. "todos" es el predeterminado y
dibuja cada bloque. "solo activos" dibuja los bloques azules, cada bloque encima de
uno de ellos, y la raíz: la bandera de vivo es la que colorea un bloque,
por lo que el interruptor nunca discrepa de los colores. Los bloques encima permanecen
porque un padre esperando a un hijo largo no escribe nada en su propia
transcripción y se lee como detenido mientras el hijo se ejecuta; sin ellos el
hijo colgaría de la nada. Con nada vivo, la vista es la raíz
sola. El interruptor reduce solo el GRÁFICO: el sello aún cuenta toda la sesión
(más "N mostrados"), y un enlace desde o
#a-<id>a un agente oculto aún abre su registro. La elección se recuerda por visor en ellocalStoragedel navegador; donde el almacenamiento está bloqueado, el interruptor aún funciona para esa visita.filterView()deformat.mjses la regla;view.test.mjsla fija. - Descripción de tarea por agente en cada bloque. Fuente de verdad: la
descriptionde la llamadaAgentque lo creó, que el arnés copia enagent-<id>.meta.json. Respaldo cuando está ausente: la primera línea del prompt de tarea del agente. La descripción se ajusta y el bloque crece para adaptarse a ella. - La última línea de registro en cada bloque, como un tooltip al pasar el ratón. Con unos pocos cientos de agentes, es lo único que separa dos bloques con el mismo tipo y la misma descripción, y para un agente vivo es lo que está haciendo ahora mismo. Viaja en el ÁRBOL porque es la única pieza de contenido de registro lo suficientemente barata como para hacerlo: texto plano, limitado, nunca HTML - en el momento en que renderiza marcado arrastra imágenes y expansores a cada instantánea.
- Modelo en cada bloque.
parseTranscriptlee el modelo de los turnos de asistente de la transcripción (el prefijoclaude-eliminado); cuando el agente aún no ha respondido, el alias grueso en su.meta.jsonsirve como sustituto. Un nodo sin ninguno de los dos no renderiza ninguna línea en lugar de un marcador de posición. - Agentes nombrados con avatares. Cada bloque muestra un nombre, su avatar, el
agentType, la descripción, el modelo y el rango de tiempo, por lo que cinco agentes concurrentes
de un tipo son distinguibles de un vistazo.
- El nombre es una etiqueta de visualización solamente: asignado desde el registro en
identities.filecomo una función pura de (agentType, orden de spawn dentro de ese tipo), recalculado desde las transcripciones en cada actualización. Nada se almacena en ninguna parte, y los agentes no conocen sus propios nombres - el arnés posee las transcripciones y este formateador es un observador de solo lectura. - Orden de spawn, no un hash del id del agente: con N nombres y N agentes un hash choca por principio del palomar; el orden de spawn no puede.
- El orden de
nameses crítico - solo añadir. Reordenar o insertar en medio de la lista renombra silenciosamente el gráfico de cada sesión pasada. avatar= la silueta del rol;portraits= arte opcional a medida para un nombre. El avatar identifica
- El nombre es una etiqueta de visualización solamente: asignado desde el registro en