Web-Hierarchieserver
Der Mensch, für den ein Agent arbeitet, befindet sich oft auf einem anderen Gerät und kann das Terminal der Unteragenten nicht sehen. Diese Fähigkeit verwandelt den gesamten Spawn-Baum in eine einzige Live-Seite, die er einmal öffnet und beobachtet: oben ein klickbarer Graph, darunter das vollständige Log des ausgewählten Agenten.
Der Installationskontext dieser Fähigkeit – das Verzeichnis, das fleet where web-hierarchy-server ausgibt – enthält zwei Dateien dieser Installation: CONTEXT.md nennt den Agenten, seinen Port und seine URL, und config.json enthält alle Werte, die die Skripte lesen. config.example.json neben dieser Datei dokumentiert jeden Schlüssel. Die Skripte finden config.json, indem sie denselben Befehl abfragen, oder lesen die Datei, die WEB_HIERARCHY_CONFIG benennt, wenn diese gesetzt ist; ein relativer Pfad darin wird relativ zu seinem eigenen Verzeichnis aufgelöst.
Führen Sie die Prüfungen und Skripte per Pfad aus:
"${CLAUDE_SKILL_DIR}/installation/check" # Läuft ein Server für dieses Konfigurationsverzeichnis, ist er aktuell?
node "${CLAUDE_SKILL_DIR}/serve.mjs" --help # Die Konfiguration, das Transkript-Root und den verwendeten Port
Die eine goldene Regel: EIN Server pro Konfigurationsverzeichnis
Ein Konfigurationsverzeichnis (CLAUDE_CONFIG_DIR) ist das Transkript-Root eines Agenten, und genau ein Server bedient es. Ein zweiter Server auf demselben Port ist der Fehler, den diese Regel verhindern soll: Welcher Prozess auch immer die Bindung gewinnt, bedient die Seite, der andere stirbt mit EADDRINUSE, und aus Sicht des Betrachters sieht die Seite in beiden Fällen gut aus. Bevor Sie einen starten, stellen Sie sicher, dass für dieses Konfigurationsverzeichnis noch kein Server läuft (installation/check beantwortet dies und startet niemals etwas).
Die dauerhafte Form ist eine Service-Unit (installation/web-hierarchy-server.service.example): maschinenstart, Restart=always, ab dem Boot aktiv, bevor Arbeiten delegiert werden, und überlebt Neustarts. Die Installation einer solchen erfordert root, was ein Schritt für den Menschen ist. Unter macOS ist es ein LaunchAgent in der GUI-Domäne des Benutzers (installation/web-hierarchy-server.plist.example), installiert ohne sudo. Wenn niemand es ausgeführt hat, läuft der Server als abgetrennter Prozess, der einmal gestartet wird (setsid nohup ... & unter Linux, nohup ... & unter macOS, das kein setsid hat, sodass er die Sitzung überlebt, die ihn gestartet hat) – diese Form funktioniert und überlebt keinen Neustart, was genau das ist, was installation/check in Worten sagt, anstatt grün zu melden.
Änderungen an serve.mjs oder format.mjs sind ABSICHTLICH latent. node lädt sie einmalig beim Prozessstart, sodass eine eingespielte Änderung eine laufende Seite erst beim nächsten Neustart erreicht. Das ist erwartet; es ist kein Grund, einen Server neuzustarten, den ein Betrachter gerade ansieht, und es IST der Grund, warum ein Bericht "meine Korrektur wurde nicht übernommen" normalerweise einen laufenden Prozess meint, der den alten Code hält.
Der Baum wird BEDIENT; die Logs werden ABGERUFEN
Dies ist die Form des Ganzen, und es ist eine Größenregel, kein Stil.
Ein Sitzungs-BAUM kostet etwa ein Kilobyte pro Agent, unabhängig davon, was dieser Agent getan hat. Seine LOGS sind unbegrenzt, und das Rendern eines Logs zieht seine Bilder mit ein. Jedes Agentenlog in jeden Snapshot zu packen, macht GET / für eine 228-Agenten-Sitzung 358 MB, davon 110 MB Base64-Bilddaten, die bei jeder 10-Sekunden-Abfrage erneut gesendet werden. Nichts ist kaputt, und die Seite kann nicht geöffnet werden.
Daher wird die Nutzlast aufgeteilt, und jede Route transportiert eine Sache:
| Route | Transportiert | Kostet |
|---|---|---|
GET / | den gesamten Baum, das Log der ROOT-Sitzung und die Bilder dieses Inhalts inline als data-URIs | ein Dokument, vollständig ohne Server |
GET /state | den gesamten Baum und KEINE Logs (?logs=<id>,<id> benennt Ausnahmen); ?view=active schränkt den GRAPHEN auf die aktive Ansicht ein | was die 10-Sekunden-Abfrage zahlt, jedes Mal, für immer |
GET /agent/<id> | genau das gerenderte Log dieses Agenten | ein Abruf, wenn der Betrachter einen Block auswählt |
GET /agent/<id>?since=<n> | nur Ereignisse ab Index n | ein leeres Array für einen ruhigen Agenten |
GET /img/<hash> | ein Bild nach Content-Hash, immutable | einmal pro Bild pro Betrachter, dann gecached |
Zwei Konsequenzen, die es wert sind, zu wissen, bevor Sie hier etwas ändern:
sinceindiziert den eigenencountdes Baums. Eine Baumzeile trägt die tatsächliche Ereigniszählung des Agenten, auch wenn sein Log nicht gerendert wurde, und diese Zahl ist das, was ein Client alssincezurückgibt. Beide stammen aus derselben Transkript-Analyse, und das müssen sie auch: Leiten Sie eine davon woanders ab und ein Client überspringt oder wiederholt still Ereignisse, mit nichts zu sehen außer einem Log, das subtil falsch ist.GET /img/<hash>ist content-adressiert, daher wird eine Antwort für immer gecached. Deshalb ist ein unbekannter Hash ein 404 und niemals ein leerer 200 – ein leerer 200 ist ein dauerhaft defektes Bild für diesen Betrachter, und kein Neuladen behebt es.
Nichts wird rot, wenn diese Aufteilung verloren geht. Packen Sie jedes Log in GET /state oder betten Sie Bilder in jeden Snapshot ein, und der Baum ist richtig, die Beschriftungen sind richtig, die Seite rendert, und sie ist hunderte Megabyte groß. Die GRÖSSE ist die zu schützende Eigenschaft; die Größentests in serve.test.mjs (6-9) sind das Einzige, was einen Rückschritt sehen kann, und jeder ist ein PAAR – hier abwesend, dort vorhanden – weil "X ist abwesend" sonst aus allen falschen Gründen wahr ist.
Was eine GESPEICHERTE Seite hat, und was nicht
GET / ist ein einzelnes eigenständiges HTML-Dokument: Inline-CSS und -JS, keine externen Assets, und ein eingebackener Snapshot, der ohne Server rendert. Eingebacken sind der gesamte Baum und das Log der Root-Sitzung, mit den Bildern dieses Inhalts als data-URIs.
Nicht eingebacken: jedes andere Agentenlog. Diese sind nicht im Dokument; sie treffen bei Auswahl von /agent/<id> ein. Daher zeigt eine auf Disk gespeicherte Seite oder eine, deren Server gestorben ist, den vollständigen Graphen und das Log der Root-Sitzung, und zeigt nichts, wenn auf einen Unteragentenblock geklickt wird. Die Bilder innerhalb dieser Logs sind die gleiche Geschichte: sie rendern von /img/<hash> und es gibt keinen Server hinter einer gespeicherten Datei.
Für einen eingefrorenen Datensatz ALLER Logs in einer Datei verwenden Sie build.mjs – der statische Bake bettet jedes Agentenlog und jedes Bild ein, weil ein Andenken nichts abrufen muss. Das ist der eine Aufrufer, der snapshot() nach allen Logs fragt (logsFor: null).
Wie es Sitzungen folgt
Ein Server ist langlebig; eine Claude-Sitzung nicht: Das Beenden einer Sitzung, ein Update und ein Unit-Neustart erzeugen alle eine neue Sitzungs-ID. Daher wählt der Server bei jedem Snapshot erneut die zuletzt aktive Sitzung aus, anstatt eine beim Start aufzulösen, und zeigt eine "Warte auf eine Sitzung..."-Seite, solange keine existiert – der normale Zustand beim Booten. "Zuletzt aktiv" umfasst das ROOT-Transkript, nicht nur Unteragenten-Dateien: Eine frisch gestartete Sitzung, die noch nicht delegiert hat, wird sofort verfolgt und als Root-only-Baum gerendert, und der Spawn-Baum wächst ab ihrer ersten Delegation. Nichts muss neugestartet werden, wenn eine neue Sitzung beginnt; die Seite erkennt die ID-Änderung und baut ihre Logs neu auf.
--session=<id> fixiert stattdessen eine Sitzung (eine eingefrorene Ansicht zum Debuggen).
Jeder Projekt-Slug unter dem Konfigurationsverzeichnis wird gescannt, nicht nur einer. Das Transkript einer Sitzung landet unter dem Projekt-Slug ihres CWD, daher schreibt eine Sitzung, die in einem Git-Worktree arbeitet, unter dem Slug des WORKTREE; das Fixieren eines Slugs macht jede Worktree-Sitzung unsichtbar, während die Seite dennoch perfekt rendert.
Welchen Agenten eine Seite anzeigt
Der Root-Block ist mit dem Fleet-Mitglied beschriftet, zu dem das Konfigurationsverzeichnis gehört, aufgelöst über config.json's fleet.by_config_dir. Der Schlüssel wird abgeleitet, nicht der rohe Basisname, und die Ableitung ist der Teil, der lesenswert ist:
fleetKey()informat.mjsnimmt den Basisnamen des Konfigurationsverzeichnisses, AUSSER wenn dieser Basisname das wörtliche.claudeist, wo es stattdessen den Basisnamen des ÜBERGEORDNETEN Verzeichnisses nimmt. Ein Fleet, das jedem Agenten ein Home-Verzeichnis gibt, platziert jedes Konfigurationsverzeichnis unter<home>/<agent>/.claude, sodass alle nach Basisname auf einen Eintrag kollidieren und die Seite jedes Agenten mit dem Agenten beschriftet ist, den dieser Eintrag benennt.
Dieser Fehler ist still: Die Seite rendert vollständig und sagt den falschen Namen, und ein Betrachter mit zwei geöffneten Seiten kann sie nicht unterscheiden und weiß nicht, dass er das nicht kann. Ein wörtlicher .claude-Eintrag wird als Fallback berücksichtigt, sodass eine Einzelagenteninstallation, die als .claude geschlüsselt ist, ihr Label behält; sie kann zwei Agenten nicht unterscheiden und sollte nicht verwendet werden, wo mehr als einer existiert. Derselbe abgeleitete Schlüssel wählt den Standardport aus config.json's servers, aus demselben Grund: Ein Basisnamen-Match würde jedem Agenten den Port des ersten Eintrags geben.
Dateien
serve.mjs– der Live-Server:GET /,GET /state,GET /agent/<id>undGET /img/<hash>(siehe die Routentabelle oben). Das primäre Werkzeug.--port=N,--host=<addr>,--cap=KB,--session=<id>, jedes mit Standardwerten ausconfig.json;--helpgibt die Nutzung, den aufgelösten Konfigurationspfad, das Transkript-Root und den Standardport aus und bindet nichts.--port=0nimmt einen flüchtigen Port und das Banner gibt den tatsächlich erhaltenen aus.format.mjs– der gemeinsame Formatierer: Transkript-Analyse, Spawn-Baum-Layout, HTML pro Ereignis, das Theme-CSS und der eine Ort, an dem lokale Werte gelesen werden (cfg()über dieconfig.json, diefleet wherenennt, oder überWEB_HIERARCHY_CONFIG, wenn diese gesetzt ist).build.mjs– optionaler STATISCHER Snapshot (node build.mjs <out.html>): backt den aktuellen Zustand in eine einzige eigenständige Datei für einen eingefrorenen, teilbaren Datensatz. Verwenden Sie dies für ein Andenken, nicht für die Live-Ansicht.identities.json+avatars/– die Anzeigenamens-Liste, verwendet, es sei denn,identities.filenennt eine andere.serve.test.mjs+fixtures/– der Test: Er startet einen echten Server auf einem flüchtigen Port gegen einen synthetischen Transkriptbaum, behauptet den Baum, treibt jede Route (Baum ohne Logs, Log eines Agenten und niemandes sonst,?sinceliefert nur das Neue, ein Bild wird Byte für Byte per Hash ausgeliefert), und LÖSCHT dann das Transkript eines Unteragenten und verlangt, dass der Baum UND die agentenspezifische Route es verlieren. Sein Header gibt an, warum jedes Bein da ist, welches Paar jedes Größenbein die Hälfte von ist und warum die Negativkontrolle der tragende Teil ist. Bein 11 verlangt, dassGET /state?view=activeden gestoppten Block fallen lässt, den die Standardansicht zeichnet.view.test.mjs– die "nur aktive"-Regel für sich allein (filterView()über einen synthetischen Baum): lebende Blöcke, die Blöcke darüber und der Root, mit den gestoppten Geschwistern und Kindern daneben fallen gelassen, und "alle" gibt jede Zeile unverändert zurück.format.test.mjs– wie die Konfiguration gefunden und gelesen wird: durchfleet where, wennWEB_HIERARCHY_CONFIGnicht gesetzt ist (ein gefakter Fleet, ausgewählt durchFLEET_BIN), die gewinnende Variable, ein fehlschlagender Fleet wird weitergegeben, nicht geraten, relative Pfade, die Listenvorgabe undpackages.dir_pattern.installation/– die Unit-Vorlage, das Installationsverfahren undcheck.
Was die Seite zeigt
Sie liest sich wie die Claude Code-Konsole im dunkelfarbenblinden (dunkel-daltonisierten) Theme – Palette wörtlich aus dem Claude Code-Bundle übernommen (schwarzer Hintergrund, weiße Monospace-Schrift, Werkzeug-Aufzählungspunkte in Erfolgsblau #3399ff, Fehler #ff6666, gedimmte Ergebniszeilen mit "... +N Zeilen"-Expandern, Denkmarkierungen, Aufgaben- und Benutzernachrichten wie Konsoleneingabe eingerahmt).
- Spawn-Baum-Graph. Die Root-Sitzung ist der oberste Knoten; jeder elternlose Agent hängt an ihr. Blau = aktiv (mit einem rotierenden Spinner-Bogen neben dem Zeitbereich; statisch unter
prefers-reduced-motion), grau = gestoppt. Klicken Sie auf einen Block, um das vollständige Log dieses Agenten unten anzuzeigen. - Ein "Alle / nur aktive"-Schalter über dem Graphen. "Alle" ist die Vorgabe und zeichnet jeden Block. "Nur aktive" zeichnet die blauen Blöcke, jeden Block über einem von ihnen und den Root: Das Aktiv-Flag ist das, das einen Block färbt, daher ist der Schalter nie anderer Meinung als die Farben. Die Blöcke darüber bleiben, weil ein Elternteil, das auf ein langes Kind wartet, nichts in sein eigenes Transkript schreibt und als gestoppt gelesen wird, während das Kind läuft; ohne sie würde das Kind an nichts hängen. Wenn nichts aktiv ist, ist die Ansicht nur der Root. Der Schalter schränkt nur den GRAPHEN ein: Der Stempel zählt immer noch die gesamte Sitzung (plus "N angezeigt"), und ein From-Link oder
#a-<id>zu einem versteckten Agenten öffnet immer noch sein Log. Die Auswahl wird pro Betrachter imlocalStoragedes Browsers gespeichert; wo der Speicher blockiert ist, funktioniert der Schalter für diesen Besuch trotzdem.filterView()informat.mjsist die Regel;view.test.mjsfixiert sie. - Aufgabenbeschreibung pro Agent auf jedem Block. Wahrheitsquelle: die
descriptiondes aufrufendenAgent-Aufrufs, die das Harness inagent-<id>.meta.jsonkopiert. Fallback bei Fehlen: die erste Zeile der Aufgabenaufforderung des Agenten. Die Beschreibung umbricht und der Block wächst, um sie aufzunehmen. - Die letzte Logzeile auf jedem Block als Hover-Tooltip. Bei ein paar hundert Agenten ist es das einzige, was zwei Blöcke mit demselben Typ und derselben Beschreibung trennt, und für einen aktiven Agenten ist es das, was er gerade tut. Es reist im BAUM, weil es der eine Log-Inhalt ist, der günstig genug ist: Klartext, begrenzt, niemals HTML – sobald es Markup rendert, zieht es Bilder und Expander in jeden Snapshot.
- Modell auf jedem Block.
parseTranscriptliest das Modell aus den Assistant-Turns des Transkripts (dasclaude--Präfix entfernt); wenn der Agent noch nicht geantwortet hat, steht der grobe Alias in seiner.meta.jsonals Ersatz. Ein Knoten mit keinem von beidem rendert keine Zeile anstelle eines Platzhalters. - Benannte Agenten mit Avataren. Jeder Block zeigt einen Namen, seinen Avatar, den agentType, die Beschreibung, das Modell und den Zeitbereich, sodass fünf gleichzeitige Agenten eines Typs auf einen Blick unterscheidbar sind.
- Der Name ist ein reines Anzeigelabel: zugewiesen aus der Liste bei
identities.fileals reine Funktion von (agentType, Spawn-Reihenfolge innerhalb dieses Typs), neu berechnet aus den Transkripten bei jeder Aktualisierung. Nichts wird irgendwo gespeichert, und die Agenten kennen ihre eigenen Namen nicht – das Harness besitzt die Transkripte und dieser Formatierer ist ein reiner, schreibgeschützter Beobachter. - Spawn-Reihenfolge, nicht ein Hash der Agenten-ID: Bei N Namen und N Agenten kollidiert ein Hash durch das Taubenschlagprinzip; die Spawn-Reihenfolge kann nicht kollidieren.
names-Reihenfolge ist lasttragend – nur anhängen. Das Neuordnen oder Einfügen in der Mitte benennt still jede Grafik vergangener Sitzungen um.avatar= die Silhouette der Rolle;portraits= optionale, maßgeschneiderte Kunst für einen Namen. Der Avatar identifiziert
- Der Name ist ein reines Anzeigelabel: zugewiesen aus der Liste bei