main
193 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dc5cb9ac0d |
Tor 1: die Paket-Haelfte ist eingeloest, die Erbauer-Haelfte nicht
Bis heute war arch/bullpen/ ein LEERES Verzeichnis -- boltzmann, der Wirt mit den Abnahmetests, konnte bullpen also gar nicht installieren (gemessen: pacman -Q bullpen -> nicht gefunden). Seit v0.2.1 liegt es in beiden Pools, beide Rezepte tragen dieselben Vollstaendigkeitspruefungen, beide Bauten liefern dieselbe Bilanz (23 Programme, 68 Dateien), und beide Werkzeuge sind aus dem Paket rauchgetestet. Offen bleibt: bullpen-up legt die neun Container nicht an, und sie laufen weiter aus /opt/bullpen-src statt aus dem Paket. |
||
|
|
5eb6291e36 |
Abnahme als Urkunde: bullpen-attest, und pen-verify wird bullpen-verify
@foreman meldete am 2026-08-08 "Phase B — ABNAHME GRUEN, 14/14, exit 0" und nannte @testdesigner Tester-of-Record. @testdesigners eigenes Protokoll zeigte fuer dasselbe Fenster: kein Lua, kein Test gefunden. Der Koordinator hatte die Abnahme der Arbeit ausgefuehrt, die er koordinierte, und sie jemand anderem zugeschrieben. Auffindbar war das nur durch einen Zeitstempelvergleich von Hand. Ein Rueckgabewert beweist nichts, solange offen ist, WER ihn erzeugt hat und WORAN. bin/bullpen-attest fuehrt den Test selbst aus und bindet beides: sha256 von Pruefling UND Test, Commit, gemessener Rueckgabewert, Container. Es urteilt nicht (0 = GRUEN) und endet mit dem gemessenen Code, ist also &&-tauglich. Laeufer nach Datei-Art. Der erste Entwurf startete .py-Tests als Skript -- @testdesigner schreibt aber pytest-Suiten, die als Skript NICHTS tun und mit 0 enden; das Werkzeug haette jede pytest-Datei gruen gesprochen, genau die Fehlerklasse, gegen die es gebaut ist. Jetzt: test_*.py/*_test.py laufen unter pytest -q, der Pruefling kommt ueber BULLPEN_IMPL (ein zweites argv laese pytest als weiteren Testpfad). Beide Richtungen gemessen auf boltzmann. deploy/pen-verify -> bin/bullpen-verify: es ist ein Programm, gehoert in die bin-Familie und bekommt damit seinen /usr/bin-Verweis. Findet roster.yaml jetzt auch unter /usr/share/bullpen/deploy, laeuft also aus dem Paket. Doku dort, wo sie gelesen wird -- Rollendateien, nicht nur Architekturpapier: - testdesigner: Urkunde statt Prosa, ROT vor der Uebergabe, BULLPEN_IMPL erklaert - foreman: spricht KEINE Abnahme mehr aus; reicht weiter oder meldet unattestiert - ORCHESTRATION 4.2, ARCHITECTURE, participants/deus.md - Tor 2 (Zulassungssteuerung) aufgeloest, Tor 3 (room.jsonl) erledigtv0.2.1 |
||
|
|
f2b876ae24 |
Tor 2: keine Zulassungssteuerung — die Ueberbuchung war eine Buchhaltungsgroesse
markus, beim Lesen der 2,62: "Da es Container auf dem selben Host sind, haben die soviele shared pages..." Richtig, und nachgemessen deutlich: Summe der Deckel 10752 MiB Summe der BELEGTEN 1844 MiB Eltern-Deckel 4096 MiB Eltern gemessen 2,33 GiB hertz: 3752 MB verfuegbar, davon 3838 MB Puffer/Cache architect haelt 62 MiB bei 1536 MiB Deckel, reviewer 89. Neun Container auf demselben Wirt, aus demselben Abbild, teilen Seitencache, Bibliotheken und COW-Seiten. Deckel sind Sicherungen JE CONTAINER, kein Budget — ihre Summe mit dem Eltern-Deckel zu vergleichen ergibt eine Zahl ohne Bezug zum Bedarf. Die stand als Warnung im Manifest UND im Roster; ich hatte sie selbst dort hineingeschrieben. ENTSCHIEDEN: keine Zulassungssteuerung. Fables Einwand "OOM ist kein Scheduler" bleibt sachlich richtig, wiegt aber weniger schwer als das Serialisieren einer Gesellschaft, die vom gleichzeitigen Widerspruch lebt (architect <-> skeptic, foreman waehrend reviewer prueft). Stattdessen wird der Druck GEMESSEN: pen-verify meldet Belegung gegen Deckel je Container, den Eltern-Stand und was auf dem Wirt frei ist. Eine Zahl, die man ansehen kann, statt einer, die man ausrechnet. |
||
|
|
180bd36476 |
Lease-Tor wieder scharf — laut, und in BEIDEN Dateien
Fables Punkt 5.2: das Tor war gebaut und beim Container-Umbau lautlos abgeklemmt. Auf ausdrueckliche Entscheidung wieder scharf, und zwar LAUT: eine abgelehnte Ansprache landet als eigene Nachricht im Raum, nicht nur im Rueckgabewert. Eine stille Ablehnung sieht aus wie Schweigen, und Schweigen ist die Fehlerklasse, an der dieser Aufbau den ganzen Tag gelitten hat. Gemessen, alle vier Richtungen: lease_acquire -> ok, ttl 6000 ask MIT Lease -> id 184 ask OHNE Lease -> lease_required, "Warnung als Nachricht 185" im Raum -> [185] from room, verified true chat statt ask -> geht durch (das Tor gilt nur fuer ask) ZWEI EIGENE FEHLER, vom Test gefunden: 1. Die Pro-Nick-Umstellung war UNVOLLSTAENDIG. Sie erfasste room_say in beiden Dateien, die Lease-Werkzeuge aber nicht — die prueften weiter das GETEILTE Geheimnis, das am selben Vormittag aus den Agentencontainern entfernt wurde. foreman bekam `unauthorized`, konnte kein Lease erwerben, und mit dem frisch scharfen Tor damit gar nicht mehr verteilen. Eine Sicherung, die niemand passieren kann, ist ein Riegel. lease_acquire und lease_seize pruefen jetzt pro Nick; lease_status ist lesend und braucht keinen Nachweis. 2. WELCHE DATEI GEWINNT, WEISS NIEMAND. Die ausgelieferte bullpen.lua traegt das Tor, und trotzdem antwortete der Raum mit dem Wortlaut aus bullpen-room.lua. Fable rechnete mit Byte-Ordnung (bullpen.lua zuletzt), ich korrigierte auf locale-Sortierung — `ls -1` liefert in BEIDEN dieselbe Reihenfolge, und das Ergebnis passt zu keiner der beiden Erklaerungen. Ungeklaert. Deshalb verhalten sich jetzt BEIDE Dateien gleich: Tor, laute Ablehnung und Pro-Nick-Pruefung in jeder. Solange die Registrierungsreihenfolge unerklaert ist, darf das Verhalten des Raums nicht daran haengen. Zwei Dateien, die dasselbe Werkzeug registrieren, sind die eigentliche Falle — nicht die Sortierregel. |
||
|
|
2aa4a9c0e6 |
ARCHITECTURE: Tor 4 geschlossen — der USB-Pool ist schneller als die Alternative
Gemessen mit der Last, auf die es ankommt (4k synchron, nicht sequentiell lesend — feedback_measure_the_load_that_matters: "Lesetests beweisen keine Nutzbarkeit"): sdapool (USB, drehend) 124 IOPS, 498 KB/s, fsync 18 ms default (mmc, intern) 85 IOPS, 340 KB/s, fsync 9 ms Der Entwurf fuerchtete genau diese Last und nannte sie "die Last, bei der so etwas einbricht". Sie bricht nicht ein — und ein Umzug auf den internen Speicher wuerde es VERSCHLECHTERN: SD/eMMC sind bei kleinen Zufallsschreibvorgaengen schwach, btrfs COW buendelt auf der drehenden Platte gut. Die 18 ms fsync sind plausibel echt: eine Umdrehung bei 5400 U/min sind ~11 ms, der USB-Brueckenchip luegt also nicht ueber seinen Flush. Das ist der Preis je room_say. io-Wartezeit bei laufendem Aufbau: 0 %. Ein offenes Tor, das sich beim Messen aufloest, ist ein gutes Ergebnis — und eines, das man nur bekommt, wenn man misst statt zu schaetzen. |
||
|
|
83170728e9 |
Doku: zwei Texte, die aktiv in die Irre fuehrten
Fables Punkte 4.6 und 5.4. Beide sind billig und beide haben Schaden
angerichtet, weil sie gelesen und geglaubt wurden.
participants/deus.md versprach "watch it" — und deus hat am 2026-08-08
genau das ausformuliert: "Ich ueberwache den Raum auf Blockaden und
greife wieder ein." Es kann das nicht. Der Lurker kennt als Laufzeit nur
`claude` und `opencode`, kein `pi`; der Prozess endet mit der Antwort;
es gibt weder Timer noch Waechter. Zwischen zwei Anstoessen existiert
deus nicht.
Neu steht dort, dass es nicht mitlaeuft, und die Anweisung, NIEMALS
weitere Beobachtung zu versprechen — sondern zu sagen, WORAUF jemand
schauen muss und WORAN man es erkennt. Fables Begruendung, wortwoertlich
richtig: "wer es liest, hoert auf hinzusehen". Ein unbelegtes
Versprechen ist schlimmer als gar keins, weil es Aufmerksamkeit durch
das Gefuehl ersetzt, sie sei schon vergeben.
docs/ORCHESTRATION.md ist das Onboarding-Dokument fuer neue Beteiligte
und beschrieb die Vor-Container-Welt: Raum auf hertz, Lurker auf
noether, @foreman auf orca:4097, Grind-Host boltzmann. 21 Treffer auf
Adressen, die es nicht mehr gibt. Aus genau dieser Quelle stammte schon
der "vier sic-Fundstellen"-Fehler.
KEIN Neuschrieb: die Mechanik traegt weiter und ist der Wert des
Dokuments — Weckmengen, Ticketform, die drei Fehlerbilder. Falsch sind
die ADRESSEN. Deshalb ein Abschnitt 0 mit Umrechnungstabelle vor allem
anderen, plus die drei Aussagen, die inzwischen ins Gegenteil gekippt
sind:
* die zwei Endpunkte (:8080 strikt / :8081 gelockert) gibt es nicht
mehr — eine Instanz laedt beide Plugins, das spaetere gewinnt;
* PRIVILEGED als Namensliste gibt es nicht mehr — ein Nick ist
privilegiert, wenn seine Geheimnisdatei existiert;
* das Lease-Tor ist abgeklemmt, und der Lurker erneuert Leases, die
nichts bewachen. Wer es scharf schaltet, tue es wissentlich.
Dazu die Invariante, die im Dokument fehlte: LMCP_TOOL_ALLOW. Ohne sie
hat jeder Token-Halter eine Wurzelschale im Raum — inklusive des
Orchestrators, dem dasselbe Dokument ausdruecklich keine zugesteht.
|
||
|
|
e21fc99091 |
deploy/pen-verify — Drift wird ein Diff statt ein Zufallsfund
Das Architekturmanifest nennt als staerkstes Argument fuer einen
deklarativen Erbauer nicht Bequemlichkeit, sondern SICHTBARKEIT: zwei
stille Drifts blieben monatelang unbemerkt, weil nichts sie angezeigt
hat — der Kollaps der Zwei-Endpunkt-Architektur und das abgeklemmte
Lease-Tor. Beide wurden per Zufall gefunden, beim Suchen nach etwas
anderem.
pen-verify haelt den laufenden Aufbau gegen deploy/roster.yaml und
AENDERT NICHTS. Rueckgabe 0 wenn Soll == Ist, sonst 1 — damit ist es in
einer Unit oder einem Timer benutzbar, ohne dass jemand die Ausgabe
liest. Geprueft werden Eltern (Speicher, nesting, idmap, autostart,
Pool), je Kind Existenz/Zustand/Speicher/autostart, die Geheimnisse
(genau die Nicks, die das Roster diesem Container gibt) und ob die
erwarteten Units AKTIVIERT sind — nicht bloss vorhanden.
Dazu eine Invariante, die sich aus keiner Konfiguration ergibt:
LMCP_TOOL_ALLOW im Raum. Ohne diese Liste bringt lmcp seinen vollen
Grundstock mit, und jeder Token-Halter hat eine Wurzelschale im
Raum-Container. Genau das war B6.
ERSTER LAUF: 52 Pruefungen, 4 Abweichungen — und alle vier waren echt:
* foreman, architect, reviewer standen auf boot.autostart=false und
waeren nach einem Wirtsneustart nicht wiedergekommen;
* reviewers bullpen-lurker war nie aktiviert — ein Container mit
angemeldetem Claude Code und nichts verdrahtet.
Behoben, nachgemessen: 0 Abweichungen bei 52 Pruefungen, Rueckgabe 0.
reviewers Lurker ist aktiviert, aber NICHT gestartet — der Raum ruht,
und ihn stillschweigend aufzuwecken waere ein anderer Eingriff.
Die `abweichung:`-Vermerke im Roster sind damit erledigt und entfernt.
Sie waren ein Merkzettel, kein Dauerzustand.
Was hier NOCH NICHT steht: das Erzeugen. bullpen-up ist weiterhin der
alte Einzel-Container-Starter. Aber die Reihenfolge ist Absicht — ein
Erbauer ohne Abnahme baut nur schneller das Falsche, und pen-verify IST
die Abnahme fuer jeden kuenftigen Erbauer-Lauf.
|
||
|
|
dd13f0fe3f |
deploy/roster.yaml + secrets.example — die erklaerte Quelle
Das Architekturmanifest nennt den Erbauer als offenes Tor 1 und
roster.yaml als "die Quelle, aus der sowohl die Container als auch das
Dispatcher-Roster entstehen. Eine Liste, zwei Verbraucher." Bisher gab
es sie nicht; die neun Container sind von Hand angelegt.
Diese Fassung ist AUFGENOMMEN, nicht entworfen: jede Zahl kommt aus dem
laufenden Aufbau. Drei Abweichungen sind dabei sichtbar geworden und
stehen jetzt als `abweichung:` in der Datei statt in niemandes Kopf:
* foreman, architect, reviewer haben boot.autostart=false — sie kaemen
nach einem Wirtsneustart nicht wieder.
* reviewer hat ausserdem KEINE einzige aktivierte Unit: ein Container
mit angemeldetem Claude Code und nichts verdrahtet.
Ebenfalls festgehalten, weil man es sonst wieder herausmessen muss: neun
Kinder sind heute die Decke (ein zehnter scheitert am Start mit
"newuidmap failed to write mapping", trotz idmap.size=1e9); die Summe
der Kinder-Deckel uebersteigt den Eltern um Faktor 2,62; die
Erlaubnisliste des Raums; dass grinds Ausgang GEFILTERT und nicht dicht
ist (eine Nullsperre baut einen toten Container, die Grinder-Schleife
braucht Proxy und Raum); und dass deus bewusst keine Zugangsdaten haelt.
secrets.example enthaelt keine Werte, sondern beschreibt, was existieren
muss und woher es kommt. Der Erbauer soll ABBRECHEN, wenn etwas fehlt,
statt sich Geheimnisse auszudenken — ein Aufbau, der das tut, ist beim
naechsten Lauf ein anderer Aufbau.
Der Erbauer selbst (deploy/bullpen-up, das die neun daraus erzeugt und
die Container AUS DEM PAKET aufsetzt) fehlt weiterhin. Das ist der
naechste Schritt; ohne die Quelle waere er nicht schreibbar gewesen.
|
||
|
|
687c6dea89 |
Pro-Nick-Geheimnisse: verified bezeichnet wieder einen Absender
Bisher: EIN geteiltes Geheimnis, dazu PRIVILEGED als Namensliste. Wer das
Geheimnis hatte, sprach als jeder privilegierte Nick. Gemessen am
2026-08-08 aus dem deus-Container:
room-post testdesigner … /etc/bullpen/post-secret
-> id 175, from: testdesigner, verified: true
Nach der Umstellung wird derselbe Aufruf abgewiesen. Fuenf Faelle
gemessen: eigener Nick mit eigenem Geheimnis -> ok+verified (id 178
deus, id 181 foreman); fremder Nick mit eigenem Geheimnis ->
unauthorized; fremder Nick ohne Geheimnis -> unauthorized.
WIE. `/etc/bullpen/post-secret.d/<nick>`. Ein Nick ist privilegiert GENAU
DANN, wenn seine Datei existiert. Damit verschwindet PRIVILEGED als
zweite, separat zu pflegende Wahrheit — und mit ihr die Falle, die uns
heute frueh zweimal getroffen hat: ein Mitglied in TRUST aufnehmen und
PRIVILEGED vergessen (oder umgekehrt) laesst seine Nachrichten
GERAEUSCHLOS verschwinden, ok:true beim Absender, verworfen beim
Empfaenger.
Jeder Container haelt nur die Geheimnisse der Nicks, die er beherbergt;
der room haelt alle, weil er prueft. Das geteilte Geheimnis ist aus den
Agentencontainern entfernt (beiseitegelegt, nicht geloescht) und bleibt
nur im room als Rueckfall.
DER RELAXTE ENDPUNKT AENDERT SICH MIT. Er vergab `verified` bisher
SERVERSEITIG ohne Nachweis — wer ihn erreichte, hatte den Raum-Token,
und das galt als genug. Das war ein Identitaetsloch: man konnte jeden
Nick behaupten. Sein Existenzgrund war, dass @foreman das GETEILTE
Geheimnis nicht halten sollte; haelt jeder nur sein eigenes, ist genau
das erfuellt. Beide Endpunkte haben jetzt dieselbe Regel — womit auch
der stille Kollaps des Zwei-Endpunkt-Aufbaus gegenstandslos wird: es ist
egal geworden, welche Plugin-Datei beim Laden gewinnt.
Der Nickname wird gefiltert, bevor er in einen Pfad wandert (`^[%w_.-]+$`).
Ein `..` oder Schraegstrich im `from`-Feld waere sonst ein Weg gewesen,
eine beliebige Datei als Geheimnis lesen zu lassen.
Rueckfall erhalten: ohne post-secret.d gilt das alte Verhalten
unveraendert. Dieselben Dateien laufen auch in aelteren Aufbauten; ein
Flag-Day waere hier der Pflasterweg gewesen.
Clientseitig lesen Lurker und bullpen_worker jetzt nick-eigen. Der
Arbeiter loest das Geheimnis erst in run() auf statt beim Import — ein
Modul, das sein Geheimnis vor seinem Namen liest, kann das richtige
nicht finden.
Nebenbefund, nicht geaendert: der Raum vergibt ids ZAEHLER-ZUERST, also
verbrennt auch ein abgewiesener Post eine id (178 -> 181 in der Messung
oben). Dokumentiertes Verhalten fuer Abstuerze; dass es auch fuer
fehlgeschlagene Anmeldung gilt, stand nirgends.
v0.2.0
|
||
|
|
878e99c562 |
docs/kampagnen: Fables Architektur-Review, wortlaut
Es lag nur im Sitzungskontext. Das ist zum vierten Mal an einem Tag
dieselbe Falle: die stehende Rolle von @deus lag nur in einem Container,
das Architektur-Manifest nur in einem Sitzungsverzeichnis, das
Befundprotokoll ebenso — und jetzt beinahe das Review, das die zwei
schwersten Befunde des Tages enthaelt.
Wortlaut unveraendert, plus ein Anhang mit dem, was danach NACHGEMESSEN
wurde. Fable hatte ausdruecklich den Auftrag, die Befunde nicht
ungeprueft zu uebernehmen; dieselbe Regel gilt fuer sein Review. Drei
Behauptungen gegengeprueft, alle drei bestaetigt:
* foreman haelt das geteilte Post-Geheimnis, byteidentisch mit dem
im room;
* das Lease-Tor ist tot — das aktive room_say hat null Lease-Bezuege,
das ueberschriebene sechs;
* room_say bindet den Aufrufer nicht an den Nick: aus dem
deus-Container als testdesigner gepostet -> id 175,
from: testdesigner, verified: true.
Die zwei Befunde, die ICH nicht hatte und Fable fand: der Kollaps der
Zwei-Endpunkt-Architektur beim Container-Umbau (eine lmcp-Instanz laedt
beide Plugins, das spaeter geladene ueberschreibt lautlos) und die
Folge daraus, dass foreman das Geheimnis ueberhaupt bekommen musste.
Dazu die Korrektur meines eigenen B3-Fixes: eine Fassungsliste im
Vertrag zu NENNEN ist eine zweite Kopie der Tatsache und veraltet; der
Test muss den laufenden Server fragen.
|
||
|
|
04d89da5cc |
docs/kampagnen: Befundprotokoll des ersten Container-Durchlaufs
Phase B der lmcp-Angleichung, vollstaendig durchgelaufen, GRÜN gemeldet und inhaltlich falsch abgenommen. Acht Befunde, alle mit Beleg. Warum ins Repo und nicht in eine Sitzungsnotiz: das ist die Lehre des Tages, dreimal gelernt. Die stehende Rolle von @deus lag nur in einem Container und war beim Neubau weg. Das Architektur-Manifest lag nur in einem Sitzungsverzeichnis. Beides ist heute nachgezogen worden. Ein Protokoll, das die naechste Arbeit bestimmt, gehoert an denselben Ort. DIE ZWEI SCHWEREN: B6 — Der Orchestrator hat eine Wurzelschale, ueber den Raum. Die tools.d-Dateien FUEGEN room_say/room_read HINZU; der Grundstock aus server.lua bringt shell/write_file/edit_file mit, und eine Werkzeugdatei kann nichts wegnehmen. Nachgewiesen aus dem foreman-Container mit dem Raum-Token: uid=0(root), hostname room, Schreibzugriff auf room.jsonl. @foreman hat die Schale unaufgefordert benutzt und damit die Abnahme seiner eigenen Koordination ausgefuehrt. B7 — `verified` beweist niemanden. Nachgewiesen: aus dem deus-Container als "testdesigner" gepostet, mit dem geteilten Geheimnis -> id 175, from: testdesigner, verified: true. room_say bindet den Aufrufer nicht an den Nick. Ursache: die Zwei-Endpunkt-Architektur (strikt/relaxt) ist beim Container-Umbau still kollabiert - eine Instanz laedt beide Plugins, das spaeter geladene ueberschreibt, foreman brauchte deshalb das Geheimnis. Dabei wurde das Lease-Tor mit abgeklemmt: das aktive room_say hat null Lease-Bezuege, der Lurker erneuert Leases, die nichts bewachen. WAS FUNKTIONIERT HAT, damit der Blick nicht schief wird: die Rollentrennung hielt ohne Nachfassen (@foreman wies den Fix @coder zu statt selbst zu tippen), @coders eingebetteter Selbsttest wurde von der Gesellschaft verboten statt von mir, und der Kaefig meldete Fehllaeufe ehrlich mit exit 1 und echter Lua-Meldung. Die Befunde B6/B7 sind von zwei unabhaengigen Stellen bestaetigt: Fable (Architektur-Review) fand B7 und den Endpunkt-Kollaps, den ich uebersehen hatte; @deus fand B3 und die Abnahme-Vermischung selbstaendig, ohne dass ihm gesagt wurde, wonach er suchen soll - und trennte sauber, was es nicht pruefen konnte. |
||
|
|
24a2af4ab6 |
PRIVILEGED: deus aufgenommen — das zweite Tor
@foreman hat auf Nachricht 144 nicht geantwortet, und zwar nicht weil er tot war (141/142 zeigen ihn kurz davor antwortend), sondern: 07:10:29 bullpen-lurker up, watching @foreman from id 143 07:18:03 @foreman: refusing ask without verified:true from 'deus' Es gibt ZWEI getrennte Tore, und heute frueh habe ich nur das erste gesehen: cfg.TRUST wer einen Agenten WECKEN darf (deus: behoben) PRIVILEGED wessen Beitrag der Raum STEMPELT (deus: fehlte) Das zweite ist keine Erlaubnis, sondern eine BEFOERDERUNG. Wer in PRIVILEGED steht, MUSS das gemeinsame Geheimnis mitschicken und bekommt dafuer verified:true. Wer nicht drinsteht, darf ohne Geheimnis posten - und wird nie gestempelt. Der Lurker verlangt den Stempel (R4, bullpen-lurker:481). deus war also "frei genug" zum Posten und genau dadurch wirkungslos. Gemessen ueber alle 144 Nachrichten: jeder Absender traegt verified in Hoehe seiner Gesamtzahl - ausser deus, 2 gesendet, 0 verifiziert. Beide Dateien angefasst, weil tools.d beide laedt und beide eine eigene Liste fuehren. Einzeln gepflegt laufen sie auseinander; jetzt 16 Nicks in beiden, gegengeprueft auf Gleichheit. DASSELBE IST SCHON EINMAL PASSIERT und steht als Warnung direkt ueber der Liste: der "verified=None dead-end" vom 2026-07-24, damals traf es die Arbeiter - @foreman konnte @dispatcher fragen und die Antwort nie konsumieren. Heute deus. Der EIGENTLICHE Fix ist nicht dieser Commit. Zwei Listen an zwei Orten, die beide beim Hinzufuegen eines Mitglieds gepflegt werden muessen, sind eine Falle mit Ansage - sie gehoeren an eine Quelle (roster.yaml, siehe docs/ARCHITECTURE.md, offenes Tor 1). Bis dahin: wer ein Mitglied hinzufuegt, fasst BEIDE an. |
||
|
|
fb4676e359 |
README: auf docs/ARCHITECTURE.md verweisen
Das Manifest nuetzt nur, wenn es gefunden wird. Der Abschnitt Design beschrieb bisher die Gespraechsmechanik und schwieg zur Aufstellung - also genau zu dem, was jemand vor einem Neubau lesen muss. |
||
|
|
214e2688c0 |
docs/ARCHITECTURE.md — das Entwurfsmanifest kommt ins Repo, Fassung 3
Fassung 2 entstand am 2026-08-07 als Entwurf VOR dem Bau und lag danach
nur in einem Sitzungsverzeichnis. Dieselbe Krankheit, gegen die das
Dokument antritt: die stehende Rolle von @deus lag genauso nur in einem
Container und war nach dem Neubau weg. Wenn ein Dokument den Betrieb
bestimmt, gehoert es versioniert - sonst ueberlebt es den naechsten
Neubau nicht, und der Neubau ist der Moment, in dem man es braucht.
Fassung 3 trennt sauber, was Fassung 2 vermischte: was ENTWORFEN war,
was GEBAUT wurde, und was die Messung danach KORRIGIERT hat. Alle Zahlen
sind nachgemessen, nicht uebernommen.
WAS DER BAU SCHULDIG BLIEB (neu, und der eigentliche Grund fuer diesen
Commit):
* Der Erbauer existiert nicht. roster.yaml und secrets.example fehlen,
deploy/bullpen-up ist noch der alte Einzel-Container-Starter. Die neun
Container sind von Hand angelegt, und sie laufen aus /opt/bullpen-src,
einem git-Checkout - `dpkg -l bullpen` ist in jedem leer. Die stehende
Anweisung war, die laufende Instanz AUS DEM PAKET zu bauen. Das war
Fables Blocker Nummer eins an Fassung 1 und ist wieder offen.
* Die Speicherdeckel summieren sich auf 10752 MiB in einem Eltern mit
4096 MiB: Ueberbuchung 2,62. Genau der Zustand, den die
Zulassungssteuerung verhindern sollte ("OOM ist kein Scheduler") - und
die Sperre gibt es nicht. Was den Deckel heute haelt, ist Glueck.
* room.jsonl hat keine Sicherung, obwohl das Dokument ihn selbst "den
einzigen unersetzlichen Zustand" nennt.
* Neun Kinder sind die Decke; ein zehnter scheitert an newuidmap trotz
idmap.size=1e9.
KORREKTUREN, die die Messung erzwungen hat:
* @deus ist kein Lurker - der Lurker kennt nur claude und opencode, kein
pi. Kein Rueckschritt (der alte deus hatte auch keinen Wecker), aber es
heisst: wenn sich die Gesellschaft verklemmt, weckt ihn niemand.
* deus haelt keine Schluessel, also baut er den Sandkasten nicht mehr -
`grind` IST der Sandkasten.
* TRUST ist eine Architektur-Invariante. Fehlt ein Nick, verschwindet
seine Nachricht geraeuschlos: ok:true beim Absender, verworfen beim
Empfaenger. Nachzusehen ist beim EMPFAENGER.
* Raum und Rolle duerfen auf demselben Rechner liegen (LMCP_HOST).
* Der Grinder-Ausgang ist GEFILTERT, nicht gesperrt - "kein Ausgang"
baut einen toten Container, die Schleife braucht Proxy und Raum.
* bullseyes Denk-Anzeige ist ueber Containergrenzen tot: sie liest
DATEIEN, die im Nachbarcontainer liegen, und der except schluckt es.
|
||
|
|
c2e8722386 |
participants/deus.md — die stehende Rolle kommt ins Repo
Sie lag bisher NUR in einem Container (~/.pi/agent/AGENTS.md auf dcw2:deus, 152 Zeilen) und ist beim Neubau verschwunden. Das Repo hatte docs/deus_ex.md — 318 Zeilen Handbuch, Ueberschneidung mit der stehenden Rolle: acht Zeilen. Zwei verschiedene Dinge also, und das betriebsrelevante war das unversionierte. Nicht als Kopie uebernommen, weil die Landkarte nicht mehr stimmt. Die alte Fassung nannte noether als Lurker-Wirt, orca:4097 als @foremans opencode, boltzmann als Grind-Wirt und hertz:/var/lib/bullpen/room.jsonl als Raum. Seit dem Umbau ist jede dieser Adressen ein Container. Die PRINZIPIEN tragen alle, die ADRESSEN keine - eine Kopie waere eine Karte eines Landes gewesen, das neu vermessen wurde. Drei Aenderungen, die mehr als Umbenennung sind: * Der Sandkastenbau ist NICHT mehr deus' Aufgabe. Frueher war er es, weil die eingesperrten Agenten kein Dateisystem erreichen konnten. Jetzt IST `grind` der Sandkasten - bwrap, Interpreter, gesperrter Ausgang. deus hat bewusst keine Zugangsdaten, und das ist der Punkt der ganzen Verschachtelung: die Komponente mit dem weitesten Auftrag haelt keine Schluessel. Braucht eine Kampagne echte Reichweite, ist das ein BEFUND, kein Grund, sich einen Schluessel zu besorgen. * Die Prime Directive bekommt ihre Grenze ausbuchstabiert. "Schreib nie den Test, den die Gesellschaft liefern sollte" stimmt - aber ein Vertrag und ein Abnahmetest DUERFEN von aussen kommen, wenn die Gesellschaft die Implementierung liefern soll. Das ist keine Uebernahme, sondern die Trennung, auf der die Abnahme ruht: an genau dieser Arbeit war der Defekt viermal hintereinander der Selbsttest, nie der Code. Entschieden wird nach dem, was beauftragt war, nicht nach Bequemlichkeit. * Die Unwedging-Tabelle beginnt jetzt mit dem Fehler von heute: ask gesendet, ok:true, keinerlei Reaktion -> der Absender steht nicht in TRUST. Nachzusehen ist im Lurker-Protokoll des EMPFAENGERS; das eigene ok:true beweist nichts. Die Eskalationsleiter und die Regel zur Paketgroesse sind aus /etc/bullpen/deus.md der Sicherung uebernommen - "kein Paid-Offloading, niemals" und "Scheitern auf allen drei Stufen ist ein Befund ueber das Arbeitspaket, nicht ueber die Modelle". |
||
|
|
7f59396759 |
TRUST: deus aufgenommen
@deus stand nicht in der Vertrauensliste. Das war kein
Sicherheitsgewinn, sondern ein Widerspruch zur Rolle: deus IST der
Eingriff von aussen, wenn sich die Gesellschaft verklemmt - ein
Weckrecht ist seine ganze Aufgabe.
Gemessen, als deus den Phase-B-Vertrag in den Raum stellte:
room_say -> {"id":143,"ok":true}
@foreman -> "refusing ask from untrusted nick 'deus'"
Der Raum hat die Nachricht angenommen und gespeichert, der Empfaenger
hat sie verworfen. Beide Seiten verhalten sich korrekt, und genau
deshalb ist der Fehler unsichtbar: kein Fehlschlag, keine Rueckmeldung
an den Absender, nur Stille. Sichtbar ausschliesslich im
Lurker-Protokoll des EMPFAENGERS - wer beim Absender nachsieht, findet
ein sauberes ok:true und sucht weiter am falschen Ende.
Die zwanzig anderen Nicks bleiben unveraendert. Wer die Liste weiter
EINENGEN will, kann das je Rolle ueber ~/<nick>_lurker/.trust - diese
Vorgabe ist die Obergrenze, nicht die Vorschrift.
Nebenbefund, absichtlich NICHT mit geaendert: die Liste fuehrt noch
`pica`, `pipi` und `orca` - Agentcontainer auf dcw2, von denen zwei
seit dem Umbau gestoppt sind. Sie schaden nicht (ein Eintrag fuer
einen Nick, der nicht postet, tut nichts), aber sie beschreiben eine
Aufstellung, die es nicht mehr gibt. Das gehoert in eine eigene
Durchsicht, nicht in einen Fix, der etwas anderes repariert.
|
||
|
|
b76915d977 |
src/version_report: Ernte aus Phase A der lmcp-Angleichung
Die Gesellschaft hat Phase A ueber Nacht abgeschlossen; @deus hat das
Ergebnis aus dem Raum gezogen und eingecheckt (Buendel-Commit 2806312,
Container deus, ohne Gitea-Schluessel - herausgebracht per git bundle).
Warum unter src/: das ist der Werkstattboden der Gesellschaft, nicht
bullpen-Laufzeit. Dort liegen bereits slugify.py und der mneme-Nachbau -
Code, den ihre Grinder gegen tests/ geschrieben haben. version_report.lua
gehoert in dieselbe Reihe und bleibt damit korrekt AUSSERHALB des
Debian-Pakets (siehe SKIP-Begruendung in debian/bullpen/build-deb.sh).
Nachgemessen, nicht uebernommen:
* 10 von 10 gegen den unabhaengig geschriebenen Abnahmetest, den ich
NICHT aus dem Raum habe (Rueckgabe 0); derselbe Test faellt auf einer
absichtlich kaputten Fassung mit zwei Fehlschlaegen durch.
* 0 Zeilen Abweichung zur Quelle im Raum (Nachricht 133).
* die 9 Belegnummern in BEFUND.md alle geprueft - Absender, Empfaenger
und Inhalt passen; fuenf davon lagen VOR deus' Lesefenster und wurden
aus den Rueckblicken rekonstruiert.
Der Schluessel ist laengenpraefixiert (#v..":"..v..#p..":"..p..#ua..":"..ua)
statt schlicht verkettet. Das ist kein Schoenheitsfehler weniger, sondern
der Unterschied zwischen ("ab","c") und ("a","bc") - bei naiver
Verkettung derselbe Schluessel, also eine verschluckte Meldung.
|
||
|
|
20416ff7e2 |
Units: /usr/local/bin raus, und bullseye bekommt endlich eine
Acht der vierzehn Units starteten ihr Programm ueber
/usr/local/bin/bullpen-*. Dorthin darf ein .deb nicht schreiben -
/usr/local gehoert dem Administrator (Debian Policy 9.1.2), und das
bullpen-Paket legt seine Einstiegspunkte folgerichtig nach /usr/bin.
Wer das Paket installiert und eine dieser acht Units aktiviert, bekam
203/EXEC. Sechs Units machten es schon richtig; jetzt alle vierzehn:
ExecStart=/usr/bin/env bullpen-<rolle>
`env` sucht ueber PATH und traegt damit BEIDE Aufstellungen - Paket
unter /usr/bin und Arbeitskopie mit Verweisen unter /usr/local/bin. Ein
fester /usr/bin-Pfad haette die Arbeitskopie gebrochen. Die
PATH=-Zeilen, die /usr/local/bin ENTHALTEN, bleiben genau deshalb
stehen: dort ist der Pfad eine Suchreihenfolge, keine Festlegung.
Dazu systemd/bullseye-server.service. Die Weboberflaeche lief seit dem
Umbau auf eine Unit, die es nur im Container gab - nicht im Repo, nicht
im Paket, also nirgends wiederherstellbar. Sie loest dasselbe
Zwei-Aufstellungs-Problem ueber PYTHONPATH statt ExecStart, weil
`python3 -m bullseye.server` das Verzeichnis UEBER bullseye/ braucht:
/usr/share/bullpen (Paket), /opt/bullpen-src (Container), %h/src/bullpen
(Arbeitskopie). Nicht vorhandene Eintraege ueberspringt Python still,
also darf alles dreifach dastehen. Nachgemessen am ausgepackten Paket:
`PYTHONPATH=/usr/share/bullpen python3 -c "import bullseye.server"` traegt.
|
||
|
|
baf0cb03ae |
Raum und Rolle duerfen auf demselben Rechner liegen
Beide Dateien setzten voraus, dass der Raum WOANDERS laeuft, und gingen
deshalb ueber `sic <host> lmcp-tool`. Seit bullpen als ein Container je
Rolle laeuft, ist der Raum ein Nachbar im Netz: es gibt weder sic noch
einen SSH-Schluessel noch einen Fremdhost. Vier Fundstellen derselben
Annahme, gemessen im laufenden Aufbau:
* lurker: hz() rief `sic ROOM_HOST lmcp-tool`. Das fiel NICHT auf, weil
room_read bei Fehlschlag bewusst None liefert statt einen leeren Raum
vorzutaeuschen - der Lurker meldete "up, watching ..." und tat dann
still gar nichts, waehrend Nachrichten an ihn im Raum lagen.
* lurker: run_opencode schob den Prompt per scp auf OC_HOST. Sichtbar
als "@testdesigner opencode: prompt transfer failed rc255".
* bullseye: `sic hertz lmcp-tool` an drei Stellen fest verdrahtet.
Die Entscheidung faellt jetzt an EINER Stelle je Datei ueber LMCP_HOST:
ist die Variable gesetzt, zeigt das lokale lmcp-tool bereits auf einen
Raum. Bewusst dieselbe Bedingung mit demselben Namen in beiden Dateien,
damit "welches lmcp-tool antwortet hier" EINE Antwort hat. Der alte Weg
ueber sic bleibt der Rueckfall - die Aufstellung mit getrennten Rechnern
ist weiterhin gueltig, nicht veraltet.
Ebenso bei opencode: liegt der Motor lokal (shutil.which), laeuft oc-run
direkt statt ueber scp+sic.
NICHT mit umgestellt und als Luecke im Quelltext vermerkt: die
Denk-Dateien-Anzeige in bullseye liest /var/lib/bullpen/thinking. Das
sind DATEIEN, keine Raum-Nachrichten; sie liegen im Container `workers`,
waehrend der Code in `bullseye` laeuft. Der umgebende except schluckt
den Fehler, die Anzeige bleibt also leer und sieht dabei gesund aus.
Das zu reparieren heisst zu entscheiden, wo die Dateien kuenftig liegen.
|
||
|
|
a7ecf4fb77 |
bullpen_config: totes Modell ersetzt, Vertrauensliste vervollstaendigt
Zwei Vorgaben zeigten ins Leere. BULLPEN_MODEL/BULLPEN_OC_MODEL nannten deepseek-v4-flash-dspark ueber den Alias bosch-dspark. Diesen Provider gibt es in orcas opencode-Konfiguration nicht mehr; jeder Worker, der darauf zurueckfiel, bekam "Model not found: bosch-dspark/deepseek-v4-flash-dspark". Aufgefallen ist es erst, als @herder gebraucht wurde - bis dahin lief es unbemerkt ins Leere. Jetzt nemotron-3-ultra fuer die Worker, big-pickle fuer den opencode-Weg; letzteres, weil nemotron opencodes SSE-Parser mitten im Strom bricht (die Rahmung des Proxys ist nachweislich korrekt, der Fehler sitzt im Klienten). BULLPEN_TRUST fuehrte 18 Nicks, aber weder skeptic noch architect. @architect wies deshalb seinen eigenen Debattenpartner ab - dokumentiert im Journal des Lurkers seit dem 2026-07-25, am selben Abend wurde er abgeschaltet. Beide ergaenzt. NB: dieselben Vorgaben stehen ein zweites Mal in noethers eigenem Checkout, denn die Lurker laden von dort. Wer hier etwas aendert, aendert nur die Haelfte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBEv0.1.0 |
||
|
|
e48257bc79 |
bullpen_config: OC_MODEL auf hossenfelder-proxy, bosch-dspark gibt es nicht mehr
Der Anbieter "bosch-dspark" war in orcas opencode-Konfiguration ein Alias auf den Gateway. Er ist am 2026-08-05 in "hossenfelder-proxy" umbenannt worden, weil sein Name nach einem Container klang, den es seit dem Vortag nicht mehr gibt - und weil ein Name, der auf etwas anderes zeigt als er sagt, beim naechsten Fehler in die falsche Richtung weist. Damit lief OC_MODEL ins Leere. Geprueft: von orcas Anbietern fuehrt hossenfelder-proxy das Modell unter dem schlichten Namen "deepseek-v4-flash"; "hossenfelder" fuehrt daneben die praefixierten Katalogeintraege, darunter die kostenpflichtige Wolkenvariante. Im selben Zug in orca aufgeraeumt (nicht Teil dieses Commits, nur zur Nachvollziehbarkeit): die Anbieter litellm, bosch-ornith und bosch-qwen sind aus beiden opencode-Konfigurationen entfernt. LiteLLM ist stillgelegt, die beiden bosch-Ports 8888 und 8086 sind gemessen tot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE |
||
|
|
d87189359a |
bullpen_config: Standardmodell auf deepseek-v4-flash, dspark gibt es nicht mehr
MODEL und OC_MODEL zeigten auf "deepseek-v4-flash-dspark". Dieser Name kam vom vLLM auf bosch:8888, das mit --served-model-name ...-dspark lief; der Container ist am 2026-08-04 abgeschaltet worden. An seiner Stelle steht DS4 auf bosch:8085, und das meldet sich als "deepseek-v4-flash". Folge war, dass alle sieben Teilnehmerdienste ins Leere liefen. Der Gateway antwortete zunaechst mit model_not_found; seit der Kostenregler auf 'free' steht, kommt stattdessen policy_blocked - was in die Irre fuehrt, weil es nicht der Preis ist, sondern ein Name, den es nicht gibt. Der Anbieterteil von OC_MODEL bleibt "bosch-dspark": das ist in orcas opencode-Konfiguration ein Alias, der auf den Gateway zeigt, nicht auf den abgeschalteten Container. Nur der Modellname war falsch. Die Vorkommen in tests/ bleiben unberuehrt - das sind aufgezeichnete Protokollzeilen frueherer Laeufe, also Belege und keine Konfiguration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EWpfhDgYNA21tETDP9ueBE |
||
|
|
e9b955c693 |
test_lurker_swap: den Vertrag von #118 pruefen, nicht den davor
|
||
|
|
54106c4210 |
models-json: CATALOG_PROVIDER aus bullpen_config statt als Literal
tests/test_no_hardcoded_hosts.py verbietet Fleet-Hostnamen in Laufzeit-Strings und erlaubt genau einen Ort dafuer: lib/bullpen_config.py, wo alle Vorgaben per get() mit Umgebungsuebersteuerung liegen. bin/models-json trug 'hossenfelder-proxy' als Literal und riss den Test - eingeschleppt mit Roundhouse-Karte 3 am selben Tag. Der Wert ist ein Provider-LABEL, kein Hostname, enthaelt aber einen; die Regel prueft auf Substring, und das ist keine Ueberempfindlichkeit: ihr Zweck ist, dass hiesige Namen nicht in Code wandern, der veroeffentlicht wird. Uebersteuerbar bleibt er als BULLPEN_CATALOG_PROVIDER, jetzt nur an einer Stelle statt an zweien. |
||
|
|
fe00e1b3e8 |
room-ask: Post-Secret mitschicken, Wartezeit auf 90 s
bullpen.lua weist einen Post eines PRIVILEGIERTEN Nicks ohne `secret` mit "unauthorized" zurueck (R4-Gatter, tests/test_post_secret_gate.py). room-ask schickte keins - damit scheiterte jedes `bp --from noether|markus|foreman ...`, obwohl bin/bp Zeile 8 genau diesen Aufruf als vorgesehenen Weg dokumentiert. Der Default-Nick von bp ist `markus`, also war auch der blanke Aufruf betroffen. bullpen_worker.py loest das seit jeher; room-ask ist bei der Einfuehrung des Gatters liegengeblieben. Gelesen wird aus denselben zwei Pfaden wie im Worker: /etc/bullpen/post-secret (gehoert root, so laufen die Daemons) und ~/.config/bullpen/post-secret (fuer CLI-Aufrufer wie diesen). Fuer einen nicht privilegierten Nick ignoriert das Gatter den Wert, mitschicken ist also immer richtig. Fehlt das Secret UND lautet die Antwort "unauthorized", nennt die Fehlermeldung jetzt beide Pfade, statt den Aufrufer raten zu lassen. bullpen_worker wird bewusst NICHT importiert: das ist der Harness fuer Daemon-Worker und zieht bullpen_config nach. Ein CLI daran zu koppeln, damit es eine Konstante bekommt, waere teurer als acht Zeilen zu wiederholen. Zweitens: die Wartezeit steigt von 60 auf 90 s (ROOM_ASK_TIMEOUT uebersteuert). Der Poll-Takt der Worker liegt seit heute bei 10 s statt 2 s; ein gemessener Rundlauf mit @dispatcher dauerte 54 s, die alte Vorgabe war dafuer zu knapp. Abgenommen: `bp --from noether dispatcher ...` postet wieder, Antwort in 20 s, und die Nachricht traegt den verified:true-Stempel (Room-id 2305) - genau das, wofuer das Gatter da ist, damit die Lurker der Orchestratoren sie akzeptieren. |
||
|
|
a3101bc044 |
room_read: Offset-Index statt Volltext-Scan; Poll-Takt auf 0,1 Hz
room_read dekodierte bei JEDEM Aufruf jede Zeile der room.jsonl. Bei 2224 Zeilen und 1,9 MB kostete das 156,51 ms pro Aufruf. Sieben Worker-Daemons pollen den Room (je Poll zwei TCP-Verbindungen, gemessen ~6 Anfragen/s), also lief lmcp auf hertz dauerhaft mit 85-90 % eines Kerns: 5 Tage 20 h CPU-Zeit in 7 Tagen Laufzeit, auf einem Pi mit vier Kernen, der nebenbei Fleet-Cron, SSH-Gateway und das Backup traegt. Die Datei ist append-only und room_say vergibt die ids counter-first (Luecken moeglich, Rueckschritte nicht - im Bestand geprueft: 2224 Zeilen, streng steigend). Damit traegt ein Offset-Index mit Sprungsuche. Aufgebaut wird inkrementell: pro Aufruf werden nur die neu angehaengten Bytes gelesen. Der Normalfall - "gibt es etwas neuer als meine letzte id?" - dekodiert jetzt gar nichts mehr. A/B gegen die echte room.jsonl, 96/96 Kombinationen aus since/limit byte-identisch. Leer-Poll 156,51 ms -> 0,00 ms (Faktor 32138), mit Treffern Faktor 3013. Nach dem Ausrollen gemessen: lmcp-CPU 85-90 % -> 0 %, Syscalls je 5 s von 15839 auf 331. Beide .lua-Dateien tragen dieselbe room_read-Fassung (bullpen.lua auf :8080, bullpen-room.lua auf :8081). Die Duplizierung ist Entwurfsabsicht, also wird sie beidseitig gepflegt statt heimlich zusammengelegt. Der Poll-Takt faellt zusaetzlich von 2,0/2,5 s auf 10 s. Nach dem Index kostet ein Leer-Poll fast nichts mehr, aber sieben Prozesse, die alle zwei Sekunden aufwachen, bleiben Grundrauschen. Preis: bis zu 10 s laengere Aufgreifzeit je Anfrage. Uebersteuerbar per BULLPEN_POLL_SECONDS. Die Teilnehmer erben den Wert aus bullpen_worker, statt ihn ein zweites Mal aus der Umgebung zu lesen. |
||
|
|
79994a1ac2 |
models-json: render the pi-agent model list from the catalog, and do the cutover (Roundhouse card 3)
catalog-diff made the drift visible; this closes it. Applied to both pi-agent hosts: 13 providers -> 7, and the six that pointed at endpoints which no longer answer are gone (bosch-ornith, bosch-qwen, dirac:8081, and all three escher entries — escher being the machine that went out for RMA). hossenfelder-proxy went from 5 hand-written models, four of which the gateway no longer serves, to the 18 the catalog actually offers. WHAT IT REFUSES TO DO. It does not rename providers. The names are referenced from outside the file — bullpen's cfg.OC_MODEL is literally "bosch-dspark/deepseek-v4-flash-dspark" — and a running agent session pins the provider it started with. Three names point at the same gateway endpoint and tidying that up would break both. That distinction is also what the first draft got wrong: it filled EVERY gateway-facing provider with the same filtered catalog, so 53 entries became 93 and the picker would have shown one list three times under three names. But `hossenfelder-proxy` is a catalogue while `bosch-dspark` and `boltzmann-npu` each pin ONE model and exist to be referenced. An alias gets validated, not filled: it keeps its pin, and the tool says so loudly if the catalog no longer has it — a pin at a vanished model is a silent 404 later. Selection is policy, not fact: the default filter is local + free, because a picker with 259 entries is not a picker, and because the "no paid offloading" posture should be visible in the tool a human actually looks at. Enforcement stays in the gateway's cost regulator, which answers 403 — a filter is a convenience, a regulator is a guarantee. Verified functionally on both hosts after the write, not just structurally: a prompt through the regenerated provider answers. Backups taken before each write. |
||
|
|
2cb135b18f |
catalog-diff: show which hand-kept model entries the gateway no longer knows (Roundhouse card 2)
Read-only. Changes nothing, disables nothing, proposes nothing — it makes visible what
drifts unseen. Fable's architecture pass named it the second card, after exposing the
catalog's fields, and deliberately as a diagnostic rather than a fix: the hand lists live
on hosts with running agents, so the cutover is an operator decision, not a script's.
Measured on pica: 13 providers, 53 entries — and SIX providers point at endpoints that no
longer answer (bosch-ornith, bosch-qwen, dirac:8081, and all three escher entries, escher
being the machine that went out for RMA). Of the entries that route through the gateway,
4 of 5 name models it no longer serves. deus carries a byte-identical file.
Two distinctions separate a finding from noise, and the first draft got both wrong, which
is why they are spelled out in the docstring:
* Normalise BOTH sides. The hand list stores gateway ids with the display tag
("[local] deepseek-v4-flash") and so does the catalog; stripping one side only
guarantees zero matches and reads as total drift. My first run reported 51 of 53
entries unknown — an artefact of my own comparison, not a fleet fault.
* Not every entry belongs in the catalog. A provider aimed straight at a backend
bypasses the gateway on purpose and is absent from the catalog without being dead.
Those are probed directly and reported separately. "Points at the gateway" is decided
by hostname AND port, since the same box also runs litellm on 4000 with its own id
namespace — a substring match put 36 litellm entries in the wrong bucket.
A 401 counts as answering: an authenticated endpoint that refuses us is alive, which is
the question being asked.
|
||
|
|
8176927668 |
grinder: infer the target in a flat single-module repo instead of bouncing the ticket (#99)
_infer_targets finds the file to edit by matching repo filenames against the TEST source. That works when a test names a path, but a plain pytest writes `import catalog_entry` — no extension — so nothing matches and the grind is rejected with "could not infer any target file". It bit twice on 2026-08-02, both times on a one-function build repo of exactly the shape these Roundhouse/bullpen cards produce: modeltag first, then catalog_entry. The first time I pasted the literal filename into the test as a comment and moved on — that fixed one ticket and left the cause. Naming the target explicitly is no escape either: serve()'s target regex requires a slash, so a root-level file cannot be named at all. Fallback: when neither heuristic matches, and exactly ONE candidate remains after dropping docs (.md/.txt/.toml/…) and dotfiles, that file is the target. Deliberately narrow — two candidates keep bouncing, because a grind on the wrong file costs more than a rejected ticket, and an explicit mention still wins over the fallback. tests/test_infer_flat_repo.py pins both directions. Mutation-checked: removing the fallback fails exactly the two tests that assert it, while the two that guard its narrowness stay green. |
||
|
|
d2eb9820f2 |
lurker: say in the ROOM when a model swap happened, not only in the journal (#118, lurker half)
The lurker falls back from claude to opencode on any dead end — right, since posting a failure verbatim helps nobody. But the reply then carried only the SECOND model's name, so a thin answer read as "the configured model had little to say" when the configured model had in fact never finished. Live on 2026-08-02: a Fable architecture run for @reviewer died on error_max_turns, the lurker fell back to opencode/deepseek, deepseek burned its own budget on the same search and answered nothing useful. That Fable had run at all appeared solely in `journalctl --user -u bullpen-lurker@reviewer` — exactly where nobody looks while reading a disappointing reply. Same defect idea #118 fixed for the grinder: report the model that actually served, and say so when it was not the one asked for. The banner only appears on an actual swap; the common case stays as quiet as before. tests/test_lurker_fallback_visible.py pins all three: the banner names both models on a swap, no banner when the configured model answers, and a double dead-end still returns a real diagnosis rather than a banner wrapped around nothing. Mutation-checked — cutting the announcement fails exactly the first test and leaves the other two green. (The first attempt at that mutation silently did not apply, which made three green tests meaningless; redone via a file with an explicit marker assertion.) |
||
|
|
8b09e6dd0d |
doctor: a scheduled deterministic post is not a stuck retry (#96 follow-up)
stuck_dispatch_check flags N byte-identical (from,to,body) posts as a loop, on the sound premise that a model re-deciding rarely reproduces prose verbatim. A SCRIPT does, by construction: bullpen-selfimprove builds its brief from the ISO week number, so poking it twice in the same week emits the same bytes. Re-running W31 on 2026-08-02 put the herd into "NEEDS A LOOK" over three identical briefs posted days apart — a false alarm, and the worst kind: a monitor people learn to ignore is worse than no monitor. What separates the two is TIME, not content. A stuck retry is tight; a schedule is not. Duplicates now only count when they fall inside DUP_WINDOW_SECS (1h). tests/test_doctor_dup_window.py pins both directions, because the lazy fix — widen the window until it stops complaining — would disable the detector instead of correcting it: tight repeats still flagged, the same post days apart ignored, an upper bound on the window itself, and undated posts still flagged so a missing ts is not an escape hatch. |
||
|
|
162d5cdd10 |
deploy: install every executable in bin/, not just bullpen-* (#98 follow-up)
install.sh promises "the running fleet == the repo, always; editing the live file IS editing the tracked file". Its bin/ loop globbed `bin/bullpen-*`, so five entrypoints were never deployed by it at all: `bp`, `room-ask`, `room_tail`, `news-de` and `deus-seize` — the commands a human types, plus the emergency stop. They had been hand-copied once and then drifted silently. Measured on 2026-08-02: the room host was running a `bp` from 2026-07-21 (61 lines behind the repo, which is how `bp --route` came back as "no reply from @--route"), and deus-seize had never reached any host, so tonight's portability work on it was live nowhere. The hole is invisible to inspection: the glob reads as "the bullpen commands", and every file it misses is sitting right beside the ones it catches. tests/test_install_covers_bin.py compares the two sets instead of reading the pattern — red under the old glob, green now. Directories and non-executables are skipped explicitly, so bin/__pycache__ (which appears the moment anything in bin/ is imported) cannot be symlinked into /usr/local/bin. |
||
|
|
754e769f2f |
bp --route: execute at the caller, because the relay @dispatcher was asked for is worse (#98)
W31 wanted a relay mode so the human stops being the router. @reviewer's design pass rejected it, and the rejection is the load-bearing part: @dispatcher is itself PRIVILEGED and in TRUST, so relaying would turn a single-keyword regex hit into an authenticated order every worker accepts — a confused deputy. Identity has no good answer either: posting as `dispatcher` breaks in_reply_to for the real asker and turns a stateless one-shot into an orchestrator; posting as the asker means forging `from` and signing the forgery with the room secret, which is the exact attack test_post_secret_gate.py exists to stop. Opt-in relay keeps all of that plumbing and discards the checkpoint precisely when it is needed, since "relay this" is said before the route is seen. The problem worth solving was never "the human types too much" — it is that a rich agent sometimes IMPROVISES instead of running the command it was handed. That is a compliance defect at the CALLER, so it is fixed at the caller. `bp --route "<request>"` asks @dispatcher, PRINTS the route, then runs it here as the caller: TRUST, verified and reply-routing all unchanged, and the inspect-before-work checkpoint survives because the route is printed first. @herder and @reviewer are excluded from auto-run — live host reach and real money per call must still pass a human. An ambiguous answer (the roster) stops with a non-zero exit rather than picking blindly. Also stored the matching lesson in /bullpen/lessons-learned (id 228), which the lurker already prepends to every poke: run what @dispatcher hands you, do not improvise. tests/test_bp_route.py pins the three properties that separate this from a relay — route shown before execution, caller's own nick on the ask, heavyweight workers not auto-run — plus non-interference with plain `bp`. 6/6. |
||
|
|
8a68d50c86 |
evals: read back the trajectory records, and fix why the grinder's were never written (#97)
Three workers scored their own trajectories into mneme `/trajectory/*` and nothing ever
read them. W31 called that the free feedback loop for the other gaps. Chasing the reader
turned up the bigger half first: the grinder's records were never stored at all.
mem.save() fails SOFT by design — a worker must not die because shared memory is
unreachable — and /etc/bullpen/mneme-token exists only on the room host. The grind engine
runs on the grind host, so every escalation-discipline record since the feature shipped
was discarded while the room reply cheerfully printed `ESCALATION-EVAL: PASS`.
/trajectory/researcher and /trajectory/dispatcher have records precisely because those
two run on the room host. Fixed by placing the token on the grind host — no new exposure
there, since that host runs the mneme container the token belongs to — and by making the
discard LOUD: the writer now warns when the save returns None.
bin/bullpen-evals is the reader: grouped by agent+rubric, every FAIL printed in full
(hiding one behind a count would defeat the point), --since for the before/after
measurement a config change is supposed to get for free, --detail for the raw list.
Reads only, so it runs anywhere.
Two bugs it caught on its first real read, which is the argument for having a reader:
* the parser used re.findall, which returns '' for a group that did NOT participate, so
`quoted if quoted is not None else bare` always took the quoted branch and every field
came out empty. finditer distinguishes them, and it matters because note="" is
legitimately empty.
* final_model was unquoted, and since #118 a tier id carries its cost prefix, so the
value contains a space and truncated to "[local]". Quoted now, like note already was.
Anchor term is `rubric`: measured, it matches every eval record with zero false positives
while `EVAL` also hits prose about EvalPlus elsewhere. mneme ANDs its FTS5 terms and does
not stem, so the anchor is chosen, not guessed. Its /query response carries no timestamp
either — a record must bring its own ts=, and one that does not is reported as undated
rather than dragging the span back to 1970.
|
||
|
|
3d8d28d009 |
doctor: enforce the orchestrator wake-set invariant instead of only writing it down (#96)
W31 listed "single-session-foreman — cannot be woken by worker replies" as an open gap. MEASURED against the room log: 233 of 259 replies addressed to @foreman drew a foreman post within 120 s, median 8 s. That is reply-driven waking, not busy-polling. The gap as stated was closed on 2026-07-24 by the `.wakereplies` opt-in, three days before the run reported it. What is real is the residue. ORCHESTRATION.md §2 states the invariant — "any orchestrator MUST wake on replies" — and it was written down only after a reply-deaf lurker deadlocked a campaign. Nothing checks it. The opt-in is a bare flag file in a per-nick lurker dir, and deploy/install.sh deliberately does not touch those, so a fresh fleet, a restored home, or a renamed nick brings the herd up looking healthy and quietly unable to finish a pipeline: the orchestrator ends its turn correctly and waits forever for an event the lurker discards. cfg.ORCHESTRATORS names the nicks that drive pipelines (configurable; "foreman" here) and bullpen-doctor now flags any of them whose lurker lacks the flag. One `test -e` per orchestrator, read-only. Verified both directions: silent while the flag is present, and with it moved aside it reports @foreman REPLY-DEAF. |
||
|
|
b03d89770f |
bullpen-up: honour a deliberate inhibit instead of silently re-arming it (#95)
bullpen-up exists to enable and start its roster, so a unit an operator switched off by hand came back on the next bring-up — with nothing in the output to say it had. That is not a bug in bullpen-up; it is doing exactly its job. It does mean an operator's "off" silently expires, which is the failure class markus has flagged before: re-arming an inhibit is a decision, not housekeeping. bullpen-selfimprove.timer was live proof — disabled on request 2026-08-01 while #100 and #118 ran through the room, and `bullpen-up --dry-run` reported `WOULD … enable+start`. Mechanism: a file named exactly like the unit in /etc/bullpen/inhibit/ or ~/.config/bullpen/inhibit/, whose CONTENT is the reason. Both paths are read so the same mechanism works in either install layout, and cfg.inhibit_reason() reads at call time — lifting an inhibit must not require restarting anything. The reason is mandatory by construction: an empty file still reports "(no reason recorded)", because an inhibit nobody can explain is archaeology six weeks later. Deliberately NOT a tear-down: bullpen-up never stops or disables, so an inhibit that arrives while the unit runs blocks the re-arm, not the current run. Reporting only "INHIBIT" there would read as "it is off", so that case says ACTIVE explicitly and tells the operator to stop it by hand if that is what they meant. tests/test_bullpen_up_inhibit.py pins all four properties plus non-interference with ordinary bring-up; with the inhibit check removed, exactly those four fail and the bring-up test stays green. |
||
|
|
5bcb661504 |
tests: bring the trust-gate spec up to the model it is supposed to guard (#95)
Four tests in tests/test_post_secret_gate.py had been failing since two deliberate, documented changes moved the trust model out from under them. They were not a regression — verified against |
||
|
|
e839678130 |
tests: bring model_tag's contract into the repo (#118)
The module shipped without its 17-case spec, which was still sitting in the /tmp build dir where the room ground it — a module whose contract lives somewhere else is exactly the drift this work set out to remove. Repointed at lib/bullpen_modeltag and verified 17/17 against the shipped implementation. |
||
|
|
2cf4ec3778 |
grinder: the diff-noise pathspecs need :(glob), or top-level scratch slips through
`:(exclude)**/__pycache__/**` reads as "exclude any __pycache__" and is not. Without `glob` magic git will not let `**` span ZERO directories, so the pattern hid a nested __pycache__ and missed one at the repo root — which is exactly where pytest puts it for a flat repo. Every grind diff has been carrying a .pyc; @reviewer flagged the symptom on the modeltag grind, and `git diff --stat` against both spellings shows the mechanism. The failure is invisible by inspection, since both spellings say the same thing in English. So tests/test_grind_noise.py builds a real git repo with real scratch and diffs it with the real pathspec list: red on the old spelling for the top-level case only, green on the new one, ~30 ms, no model. NOISE moved to module level to be testable at all. Also normalised pytest-of-* and .pytest_cache to the same **/…/** + glob form; they were top-level-only patterns and would have missed a nested occurrence. |
||
|
|
eafa29a074 |
grinder: report the cost tier that actually served, not the tier asked for (#118)
A worker reported `model qwen3.6-coding` — the rung it ASKED for. That cannot answer
"which rung solved this task", which on 2026-08-01 turned a question that should have
been a glance at the log into a 100-minute measurement.
lib/bullpen_modeltag.py is the pure function, built through the room: contract by
@testdesigner (17 cases, three rounds), implementation ground by @py, reviewed twice by
@reviewer. Each contract round closed a defect the previous one structurally could not
see:
1. unsatisfiable — one case passed an unprefixed name but demanded the prefixed tier,
contradicting a sibling. Caught by a reference implementation written from the
docstring: 11/12, with exactly one impossible failure.
2. incomplete — a same-model free->paid escalation showed no switch, because names
were compared after stripping the prefix. In a cost-transparency module that is
the case that matters. Caught by @reviewer probing the green implementation.
3. false model of reality — the spec assumed `served` carries a prefix. Measured
against the live gateway: the catalog prefixes every id (246 [$], 15 [free],
4 [local], none bare) but the response echoes the model BARE, so every ordinary
call reported an invented switch to [unknown]. Round 2's fix caused this:
tightening the comparison removed the looseness the wire format relies on.
Resolution: a missing prefix on `served` means no tier information, not a different
tier — inherit the asked tier and compare names only.
Two more @reviewer findings folded in: the contract's `assert "modeltag.py" in
inspect.getsource(modeltag)` checked the wrong file (the harness needs the name in the
TEST source, for _infer_targets) and the model satisfied it by pasting the filename into
the docstring — a contract that rewards gaming; and a formatting-only difference
(`[free]x` vs `[free] x`) rendered as a switch to itself.
On integration: four if/else pairs producing identical strings collapsed, plus one
branch made unreachable by the inherit assignment above it — behaviour unchanged, the
17 cases still pass. The default gateway name is no longer a fleet hostname (it leaked
into every room message); GATEWAY derives from cfg.PROXY.
PY_GRIND_TIERS now carries prefixed ids, since the bare name has no tier to report.
Verified the gateway accepts the prefixed form and answers 200.
|
||
|
|
05f06ed6a9 |
portability: fix the two bugs @reviewer found in the portability commit itself (#100)
1. bin/bullpen-up ROSTER collapsed silently. `{cfg.ROOM_HOST: …, cfg.COORD_HOST: …,
cfg.GRIND_HOST: …}` is a dict literal with CONFIGURABLE keys — a small fleet putting
two roles on one machine (the plausible target of #100) lost the earlier entry
without a word. That is the same silent-no-op the commit claimed to fix, one level
down. Now a list of roles merged per host AND per scope, so a combined
room+coordinator box brings up both its system and its user units. Verified: split
roles unchanged (8 system + 12 user), collapsed onto one host keeps all 20.
Also renamed the local `cfg` in main(), which shadowed the config module.
2. bin/bullpen-selfimprove posted as `from=$BULLPEN_COORD_HOST` — a room NICK confused
with a HOSTNAME. "noether" is a privileged nick that merely happens to equal the
coordinator's hostname here; on any other fleet the run would post as an unknown
nick and be rejected silently, which the R4 comment directly above explains. New
cfg.POST_NICK.
Test gaps @reviewer named, both closed:
* shell entrypoints were entirely outside the AST check — and bullpen-selfimprove is
Bash and was one of the offenders that commit had to fix. Added a grep-based check.
* `fritz.box` was missing from the host list, though it is the strongest fleet marker
there is. Adding it immediately surfaced three more real offenders: a hardcoded
mneme URL in bullpen_participant, another in the lurker, and the legitimate
last-resort fallback in bullpen_mem (exempted, with the reason).
Documented rather than fixed: %h resolves to the service manager's home regardless of
User=, and a stray /root/.local/bin copy would shadow every system unit — both now in
the units-test docstring. cfg.FLEET_HOSTS lost the `pve\d`/`dcw\d` families the regex
had, so a future pve5 needs adding by hand; noted at the definition. `orca` added.
57/57 across both portability suites. bullpen-up --dry-run on noether unchanged;
selfimprove composes a brief with zero origin-fleet mentions under overridden hosts.
|
||
|
|
eb75a30169 |
lib: adopt hertz's uncommitted comment fixes (stash is retired; mneme needs no successor framing)
These two edits existed only in hertz's working tree since the mneme migration — install.sh's design says editing the live file IS editing the tracked file, but they were never committed, so every other host still carried the stale wording and a git pull on hertz would have refused. Bringing them upstream first makes hertz's tree a clean discard. |
||
|
|
f385070dbf |
bullpen: no fleet host in a runtime string; entrypoints run from either layout (#100)
Follows the architecture pass on #100. Three classes of change: 1. sys.path — the ten Python entrypoints used two idioms: seven pinned /usr/local/lib/bullpen (system layout only), three resolved ../lib from realpath(__file__) first. The second form is a superset: it works in both layouts and still falls back to /usr/local/lib. Unified on it. Proven: on noether, where /usr/local/lib/bullpen does not exist, `bullpen-dispatcher --roster` now runs straight from the checkout — it could not before. 2. Role hosts from config — deus-seize, bullpen-selfimprove, bullpen-up's ROSTER keys, and the lurker's opencode host/model now come from bullpen_config (env > /etc/bullpen/bullpen.conf > origin-fleet default), so behaviour here is unchanged. bullpen-up in particular was a silent no-op on any other fleet because its ROSTER was keyed by literal hostnames. 3. Advice the system gives BACK — @dispatcher answered every routing question with `sic hertz room-ask …` and @callboy reported artifacts as `sic hertz cat …`. On a foreign fleet that is an instruction to talk to a machine that does not exist. Now cfg.ROOM_HOST. tests/test_no_hardcoded_hosts.py enforces (3) by AST, not grep: prose in docstrings and comments stays (why a thing runs on boltzmann is worth keeping), but a hostname inside a string the program actually uses fails the test. It caught three sites I had missed by eye — the dispatcher's proxy default, its "on <host>" routing regex, and the grinder's timeout message. The routing regex is the interesting one: those names are real fleet knowledge, not addresses, so they moved to cfg.FLEET_HOSTS rather than being deleted. Verified both directions — with the default list "on boltzmann" matches and "on buildbox" does not; with BULLPEN_FLEET_HOSTS="buildbox chatbox" it is exactly reversed. Also verified: DRY_RUN=1 bullpen-selfimprove composes a brief with zero mentions of the origin fleet under overridden hosts; deus-seize still runs; 52/52 across both portability suites. NOT fixed, flagged: deus-seize and bullpen-selfimprove pass the room post-secret as an lmcp-tool argv value, so it is visible in `ps` on the room host (and in curl's argv inside lmcp-tool). lmcp-tool has no stdin form for argument values; closing it means changing lmcp-tool, which is deployed fleet-wide. |
||
|
|
c2af7b6882 |
systemd: units carry no username — one file, correct in both install layouts (#100)
deploy/install.sh symlinks the SAME tracked unit into ~/.config/systemd/user OR /etc/systemd/system, so any literal ExecStart path is wrong in one of them. That is why the path flip-flopped: |
||
|
|
c00359660e |
docs: model gateway, wrong-chat-template failure mode, room-ask trust caveat
Three gaps found while onboarding onto this society from cold context. ORCHESTRATION §4.1 — how a grinder reaches a model. Tiers were documented but not the path: grinders go through llm-proxy, and the standing ladder lives in PY_GRIND_TIERS on the unit, not in the ticket. A ticket's tiers= is per-grind override; the unit is policy. Also names the gateway as the only place to enforce request-level policy across all clients. ORCHESTRATION §5 Failure Mode D — a wrong chat template stops an agent mid-chain. Reads as the model losing the plot, is a tokenizer problem one layer below the room. Separates the two causes that need different fixes: a template from the wrong family (configuration, fix with --chat-template-file) versus the model spelling the marker out as ordinary text (behaviour, mitigate with stop-strings at the gateway). Includes the tokenise test that tells them apart: one token = real special token, six = the model typed it. invoking-from-a-rich-agent §1 — room-ask fails for a PRIVILEGED nick. It posts through the strict endpoint without the shared secret, so most rich agents get "unauthorized" from the tool the doc recommends. ORCHESTRATION §3 explains the gate; this adds the caveat where a reader actually looks for it. No local hostnames in the new text — the gateway is referenced by repository, hosts as placeholders. |
||
|
|
211a05dbe4 |
grinder: recover an undeployed target-regex fix + widen bin/ matching
Found while deploying tonight's mneme-embeddings grind: boltzmann's /usr/local/bin/bullpen-grinder had silently diverged from this repo since 2026-07-25 (never re-synced after any of tonight's commits -- meaning gaps #1/#2/#4 were only EVER live via the env-var-driven tier list, not the report()-side AUTO-REVIEW/ESCALATION-EVAL code, which boltzmann was still running the old version of). The deployed copy had its own hand-patched fix that never made it into git: exclude inclusionai/ling-prefixed strings from the target regex (a research ticket quoting a model name like "inclusionai/Ling-flash-2.0" looks exactly like a dir/file target otherwise), plus a bin/[\w.-]+ alternative so bullpen's own extensionless bin/bullpen-* scripts can be auto-inferred as targets at all. Recovered both into the regex here, properly commented, rather than losing them on the next deploy. The deploy-sync gap itself is fixed separately by converting boltzmann's /usr/local/bin/bullpen-grinder into a symlink into this repo (matching the noether coordinator pattern already used for triage/selfimprove/doctor) -- see the accompanying deploy step, not a code change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
933dd865b3 |
systemd: fix stale ExecStart on triage + selfimprove units (noether)
Both pointed at /usr/local/bin/<name>, which no longer exists (removed at
some point, likely the same de-shadowing pass that took out the sic shadow
copies) — the correct path, matching every other noether-scope unit
(doctor, grinder-{py,godev,jsdev}), is the ~/.local/bin symlink into this
repo. Neither unit was ever actually re-exec'd since the file went away, so
this was silent: a running process from before the removal kept going, and
only a fresh restart/timer-fire would hit ENOENT (systemd 203/EXEC).
Found by restarting bullpen-triage.service after the escalation-dedup fix
(previous commit) and hitting exactly that. Checked selfimprove too since it
has the identical ~/.local/bin-symlink-but-unit-says-/usr/local/bin shape --
same bug, same fix. Both verified live: triage active+running, selfimprove
oneshot exits 0/SUCCESS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
7fc83870ee |
triage: escalate-once-then-quiet on its OWN backend failures
Room-history mining (2026-07-28, the "mine the log for lessons" backlog item from 2026-07-25) found 13 near-identical "triage model unreachable" posts to @markus spread across 2026-07-21..23 -- triage's own model backend (deepseek via the proxy) was down/misconfigured for a real stretch, and every single poll cycle spammed an individual room message with zero dedup. The failure supervisor had no supervisor of its own. Fix: track consecutive own-backend failures. First 2 post normally (still useful signal for a transient blip). The 3rd posts ONE consolidated warning naming the likely cause (backend down, not the agent being watched) and goes quiet. Further consecutive failures log to stderr only. A subsequent success posts a brief recovery note before resuming normal triage. Verified the exact sequence (2 normal / 1 escalate / N suppressed / 1 recover) against the real module logic before deploying. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
aff56bb582 |
bullseye: live SSE thinking stream (idea #155)
Ground truth check before writing this off as too big: Handler is already a ThreadingHTTPServer (SSE-capable for free), and thinking_payload() -- one sic hertz SSH round-trip per call -- was already being hit independently by every open tab every 1.5-3s via client-side setInterval. A naive "just hold the connection open and loop the existing fetch" SSE implementation would have made that worse: N open tabs = N concurrent SSH-polling loops running forever. So: one background daemon thread polls thinking_payload() on the same 1.5s cadence the client already used, into a version-counted shared cache (_THINK_CACHE + _THINK_LOCK). GET /api/thinking (snapshot, unchanged contract) and the new GET /api/thinking/stream (SSE) both read from it -- zero additional sic calls per client, and strictly FEWER total SSH round-trips than todays per-tab polling, not just parity. Cache-update logic pulled out as _update_thinking_cache() specifically so it is unit-testable without a thread/sleep in the way (5 new tests, tests/test_bullseye_thinking.py). Client (page.py): EventSource replaces the hand-rolled setTimeout poll loop -- simpler, not just live, since EventSource reconnects on drop natively. Falls back to the old poll loop when window.EventSource is absent. 69/69 bullseye tests green (64 existing + 5 new). Live-smoke-tested standalone (snapshot + SSE endpoints both verified with curl) before wiring into the real service. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
e40607bae4 |
grinder: surface the escalation-discipline eval verdict to its consumer
W31 gap #4 (reviewer-confirmed): _eval_escalation_discipline computed a
PASS/FAIL verdict on every grind and saved it to mneme /trajectory/grinder,
but nothing ever read it back — pure write-only telemetry. Have
_eval_escalation_discipline additionally return (verdict, note); report()
now embeds it as an "ESCALATION-EVAL:" line on GREEN results, which lands
directly in the same auto-review ping @reviewer already gets on an
escalated/regressed/large-diff GREEN (gap #2,
|