Ctrl K
ComunidadGuías…

docs/governance.de.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
---
source_sha: "246a8fe6bac4"
---

# Governance { #governance }

Budgets, Freigaben, Alerts und der Audit-Trail. Die vier Dinge, die eine
Agent-Plattform zu etwas machen, hinter das Sie eine Kreditkarte legen können.

Sie alle gelten auf jeder Oberfläche gleich, weil jede Oberfläche durch einen
einzigen Runner läuft.

Die jüngste dieser Oberflächen ist ein **Trigger**: ein Agent, der sich selbst
nach einem Zeitplan oder auf ein eingehendes Ereignis hin ausführt — ein
GitHub-Issue, eine eingehende E-Mail (siehe [Konzepte](concepts.md#trigger)).

Er nutzt dieselbe Kostenerfassung, Bestätigungswarteschlange und Prüfungsspur.
Die [Richtlinie für unbeaufsichtigte Aktionen](#routine-action-consent) verschärft,
welche Aufrufe eine Bestätigung brauchen; ausdrückliche dauerhafte Zustimmung
benennt einzelne Aktionen und eine Version.

Unbeaufsichtigte Arbeit bestimmt auch den Umgang mit einer **Ablehnung**:

- Ein ausgelöster Run, den das Budget stoppt, endet mit `budget_exceeded` und
  **wartet auf das nächste Auslösen**, statt wiederholt zu werden.
- Ein ausgelöster Run, den sein Ersteller nicht mehr machen darf — er hat die
  Organisation verlassen oder seinen Grant auf den Agent verloren —
  **deaktiviert den Trigger** und schreibt einen Audit-Eintrag, der sagt, warum.
- Ein ausgelöster Run, der am Modell selbst scheitert — ein Ausfall beim
  Provider, ein widerrufener Schlüssel — wird als `failed` festgehalten und dem
  nächsten Auslösen überlassen, statt den Heartbeat dazu zu bringen, dieselbe
  Wand erneut anzulaufen. Das `last_run_id` des Triggers zeigt weiterhin darauf,
  damit seine Historie ehrlich bleibt.

Eine Ablehnung, die ein Heartbeat jede Minute wiederholt, wäre eine Rechnung oder
ein Alert, die niemals aufhören.

!!! warning "Nicht geschluckt wird ein Fehlschlag, der nie zu einem Datensatz wurde"

    Wenn der Run einen Endzustand erreicht hat, der Schreibvorgang, der ihn
    festhält, aber nicht committet wurde — ein Transkript oder ein Schreiben des
    Conversation-State, das eine Exception auslöste, nachdem die Antwort
    vorlag —, **lässt das Auslösen den Flow scheitern**, statt Erfolg zu melden.
    Der halb geschriebene Run wird zurückgerollt, statt als vollständig committet
    zu werden, ohne dass etwas dahintersteht.

Ein Event-Trigger fügt an seinem Rand eine weitere Ablehnung hinzu: ein Webhook,
dessen Signatur sich nicht gegen das eigene Secret des Triggers verifizieren
lässt, ist ein 403, der den Runner überhaupt nie erreicht.

## Zustellung von Routinen { #routine-delivery }

Eine im privaten Chat über das Plattform-MCP erstellte Routine antwortet in diesem
Chat, sofern Sie kein anderes Ziel wählen. Ihre Karte zeigt den aktuellen Zustand,
die nächste Aktivierung sowie die vorhandenen Aktionen zum Bearbeiten, Pausieren
und Entfernen. Das Ausführungsprotokoll bleibt unter Routines verfügbar.

Vor dem Speichern einer fertigen Antwort prüft die Plattform erneut die aktive Mitgliedschaft der erstellenden Person und ihr aktuelles Schreibrecht im gewählten Chat. Ein gelöschtes oder unzugängliches Ziel lässt die Antwort im Ausführungsprotokoll und zeigt **Letzte Antwort im Chat nicht verfügbar** an der Routine. Ein Datenbankfehler während der Nachrichtenzustellung wird isoliert, sodass der abgeschlossene Lauf weder rückgängig gemacht noch der Agent erneut ausgeführt wird.

Abschlussmeldungen verlinken den Chat erst nach erfolgreicher Zustellung.

`last_delivery_status` unterscheidet Zustellung, nur Protokoll, keine Antwort und einen unvollständigen Lauf; historische Einträge bleiben bis zum nächsten Lauf unbekannt. Die Migration `0265_trigger_delivery_status.py` ergänzt diesen Status, ohne frühere Erfolge zu erraten.

Setzen Sie `ends_at` mit UTC-Offset für zeitlich begrenzte Aufgaben und
`monthly_budget_usd` für ein engeres Monatslimit. Das MCP-Aktualisierungstool kann
beide Einstellungen entfernen. Ein gewöhnlicher API-Schlüssel kann keine Routine
zur Nutzung der persönlichen Konten seines Ausstellers ermächtigen. Hintergrundläufe,
Identitätsübernahmen und fortgesetzte Gruppen- oder öffentliche Runden erteilen
ebenfalls keine solche Zustimmung.

Die minütlichen Durchläufe erledigen abgelaufene Endtermine unabhängig von der nächsten Aktivierung, auch bei pausierten Routinen und Beobachtungen. Sie deaktivieren aktive Arbeit, schließen offene Blockaden der Routine und stornieren deren ausstehende Benachrichtigungen ohne Modellaufruf. Nach einem Neustart holt der Worker dies nach; die Bereinigung kann auf einen Worker oder einen weiteren Stapel warten, während Prüfungen vor Werkzeugen und Antwortzustellung abgelaufene Arbeit sofort ablehnen.

Die Migration `0269_routine_end_cleanup.py` ergänzt eine Markierung des erledigten Endtermins und partielle Indizes für ausstehende Enden. Sie erstellt keine Ersatzroutinen, Freigaben oder Funde. Eine Änderung des Endes aktiviert die Bereinigung erneut; ein wiederholter Durchlauf wiederholt keine erledigten Effekte. Ein Downgrade entfernt nur diese Hilfsdaten, nicht das konfigurierte Ende oder die Identität der Routine.

Eine beobachtete MCP-Verbindung speichert auch ihre Autorisierungsgeneration. Eine abgeschlossene erneute OAuth-Zustimmung oder ein ausdrücklich ersetztes Token beziehungsweise eine geänderte Adresse macht die bisherige Quellenzustimmung ungültig, selbst wenn die Verbindungs-ID gleich bleibt. Normale Token-Erneuerung, Bezeichnungen und Änderungen des Standardkontos ersetzen diese Identität nicht. Eine erneut geprüfte Quelle gilt für spätere Läufe; ein alter angehaltener Lauf kann die neue Zustimmung nicht übernehmen.

Prüfe nach der Migration `0270_mcp_authorization_generation.py` die Quellen bestehender Beobachtungen ausdrücklich erneut (`review_sources=true` oder **Angehängte Quellen erneut freigeben** im Editor). Alte Richtlinien bleiben lesbar, erraten aber nicht das Konto hinter einer historischen Autorisierung. Für ein Konto außerhalb der zugelassenen Generation werden keine Anbieter-Zugangsdaten gelesen. Leere alte Worker und aktualisiere API und Worker gemeinsam. Pausiere Beobachtungen vor dem Downgrade; setze sie erst mit generationsprüfendem Code und geprüften Quellen fort. Ersetzte Zugangsdaten leeren die Werkzeugliste des alten Kontos bis zur nächsten erfolgreichen Prüfung.

Routinenläufe behalten die für diese Ausführung zugelassenen MCP-Konten und Werkzeugaliase. Fortgesetzte Aufrufe verwenden dieselben Aliase: Umbenennen oder ein anderes Standardkonto leiten eine bestehende Freigabe nicht um. Delegierte bleiben innerhalb der zugelassenen Konten. Vor Aufrufen werden Kontoinhaber, Adresse, Autorisierungsgeneration und Werkzeuglisten erneut geprüft; neue oder geänderte Konten werden vor dem Lesen der Zugangsdaten ausgefiltert.

Eine angehaltene Routine ohne diesen Zulassungsstand kann MCP-Aufrufe verwalteter Konten nicht fortsetzen. Starte nach Prüfung des Zugriffs einen neuen Lauf. Die Vorgaben interaktiver Agenten und ausdrückliche Bestätigungsrichtlinien bleiben unverändert; kontospezifische Freigabeausnahmen für Routinen sind von dieser Quellenprüfung getrennt.

Die Versionsauswahl folgt entweder der standardmäßig veröffentlichten Version oder legt eine unveränderliche Version fest. Eine Veröffentlichung verschiebt diese Festlegung nicht. Entfernen Sie sie, um wieder der Standardversion zu folgen. Eine benannte Umgebung und eine festgelegte Version schließen sich aus; aktuelle Zugriffsrechte, Budgets und Freigaben gelten weiterhin.

Im Routineformular können Sie das Ende festlegen oder entfernen. Das Datum verwendet die am Feld angezeigte Gerätezeitzone, unabhängig von der Cron-Zeitzone, und wird mit UTC-Offset an die API gesendet. Änderungen anderer Felder lassen das bestehende Ende unverändert.

Eine Routine prüft vor jedem Werkzeug ihren Ersteller, aktive Mitgliedschaft, Rolle und Ausführungsrechte erneut, auch bei Delegationen und nach einer Freigabe. Pausieren, Löschen, Ablauf, Widerruf persönlicher Zustimmung oder Änderungen dieser Berechtigung verhindern weitere Werkzeugaufrufe. Bereits an externe Dienste gesendete Effekte werden dadurch nicht zurückgerufen. Migration `0260_routine_run_origin` bewahrt den Ursprung beim Löschen; ältere geparkte Zeitpläne mit verlorenem Ursprung werden abgelehnt, statt die Rechte der freigebenden Person zu übernehmen.

## Budgets { #budgets }

!!! abstract "Drei Ebenen, und sie sind keine Varianten einer Zahl"

    Das Cap eines Agents, gemessen an der Gesamtsumme der Organisation, wird von
    den Runs seiner Nachbarn aufgebraucht; das der Organisation, gemessen an
    einem einzelnen Agent, ist überhaupt keine Obergrenze. Siehe
    [warum sie sich nicht zusammenlegen lassen](#why-they-cannot-be-collapsed).

| Ebene | Gesetzt in | Misst | Angehoben von |
| --- | --- | --- | --- |
| **Agent, monatlich** | dem Spec des Agents | den eigenen Runs dieses Agents | wer den Agent bearbeiten darf |
| **Abteilung, monatlich** | den Einstellungen der Abteilung | den Runs, die ihre Mitglieder gestartet haben, auf jedem Agent | wer `members:manage` und `budgets:manage` hält |
| **Organisation, monatlich** | den Einstellungen der Organisation | jedem Run _und_ jeder Ingestion in der Organisation | wer `budgets:manage` hält |

Eine **neue Organisation startet mit bereits gesetzter Obergrenze für die
Organisation** — dem `DEFAULT_ORG_MONTHLY_BUDGET_USD` des Deployments
([Konfiguration](configuration.md), ab Werk 100 $) —, damit ein frischer Tenant
nicht nur einen entlaufenen Agent von einer überraschenden Rechnung entfernt ist.
Ab diesem Punkt ist es ein gewöhnliches Cap: auf der Zeile der Organisation
bearbeitbar und genau wie ein von Hand gesetztes durchgesetzt. Ein Deployment,
das lieber ohne Cap startet, lässt diese Einstellung leer; so oder so wird das
Cap einer bestehenden Organisation dafür nie geändert.

### Warum sie sich nicht zusammenlegen lassen { #why-they-cannot-be-collapsed }

Das waren sie einmal, mit `min()`, und das Ergebnis war falsch. Das Cap eines
Agents, gemessen an der Gesamtsumme der Organisation, wird von den Runs seiner
Nachbarn aufgebraucht, und genau das macht es zu keinem Cap. Das Cap einer
Organisation, gemessen an den Ausgaben eines einzelnen Agents, würde nie binden.

Also misst jedes Cap seine eigene Größe, und die Abfrage reist mit dem Limit. Ein
Agent mit 5 $ unter einer Obergrenze von 50 $ bindet jetzt, wenn _er_ 5 $
ausgegeben hat, und die Ablehnung nennt das Cap, das tatsächlich gebunden hat,
statt es daraus abzuleiten, welche der beiden Zahlen kleiner war.

Ein Agent kann die Obergrenze der Organisation weiterhin nicht lockern: Der
Eintrag der Organisation steht mit seiner eigenen Zahl da, was auch immer der
Spec verlangt, und die Ausgaben eines Agents sind Teil derer der Organisation —
ein Agent mit 100 $ unter einer Organisation mit 10 $ wird also bei 10 $
gestoppt.

Beide Caps sind dort lesbar, wo ihre Ausgaben stehen: das der Organisation auf
ihrer eigenen Zeile (`GET /orgs/{org_id}`) und das jedes Agents als
`budget_monthly_usd` in der Agent-Liste — die Zahl der _veröffentlichten_
Version, denn das ist die, die der Runner durchsetzt, und nicht das, was der
Draft gerade verspricht. Die Headroom-Karte des Dashboards verbindet diese mit
`GET /spend`, sodass ein Cap im Anmarsch zu sehen ist, bevor `budget_exceeded` in
der Run-Historie auftaucht.

### Das Cap einer Abteilung { #a-departments-cap }

Eine [Abteilung](departments.md#a-departments-budget) kann ein eigenes
monatliches Cap tragen. Es liegt zwischen den anderen beiden: Ein Run wird gegen
das Cap des Agents geprüft, dann gegen jede Abteilung mit Cap, in der die Person
ist, die ihn gestartet hat, dann gegen das der Organisation. Jede Abteilung misst
den Monat ihrer eigenen Mitglieder, sodass eine Person in Finanzen und Vertrieb
von derjenigen gestoppt wird, die zuerst ausgeschöpft ist, und die Ablehnung
nennt sie - _Sales department monthly budget exhausted_.

Die Leitung der Abteilung und die Administratoren werden einmal benachrichtigt,
wenn sie 80 % ihres Monats überschreitet, und erneut, wenn ihr Cap einen Run
stoppt. Ein Run, den niemand gestartet hat - ein Zeitplan, ein Besucher in einem
Kanal - gehört zu keiner Abteilung und wird nicht gezählt.

### Durchgesetzt wird vor der Anfrage { #enforcement-is-before-the-request }

!!! danger "Vorher, nicht nachher"

    Hinterher zu prüfen heißt, dass die Anfrage, die das Budget gesprengt hat,
    bereits bezahlt war — und eine Schleife kann jedes Mal um einen teuren
    Aufruf überschießen.

!!! important "Ein fehlgeschlagener Run hält trotzdem fest, was er ausgegeben hat"

    Ein Budget, das Fehlschläge ignoriert, ist kein Budget. Die Abrechnung
    geschieht auf jeder Oberfläche in einem `finally`-Block, und der Commit ist
    explizit, statt dem Session-Kontext überlassen zu werden — der bei jeder
    Exception zurückrollt und bei einer Cancellation überhaupt nie erreicht wird.

!!! important "Eine Baseline ist das, was _andere_ Runs ausgegeben haben"

    Beide Abfragen lassen die eigene Zeile des fragenden Runs aus. Ein
    fortgesetzter Run behält seine Zeile, und bis dahin hat `finish_run`
    committet, was er ausgegeben hat — während das Ledger mit derselben Zahl neu
    befüllt wird, was auch nötig ist, sonst würde der Abschluss der Fortsetzung
    die Kosten mit dem überschreiben, was allein die Fortsetzung gekostet hat. An
    beiden Stellen gezählt, kam ein bei 10 $ gedeckelter Agent, der 6 $ ausgegeben
    hatte und dann parkte, bei seiner ersten Modellanfrage auf `6 + 6 = 12` und
    wurde mit 4 $ Luft abgelehnt — und teilte seinem Besitzer mit, er habe ein Cap
    erreicht, bei dem er zu 60 % stand. Das Cap der Organisation zählte identisch
    doppelt.

Der Guard ist ein harter Stopp für einen Run, der die Ausgaben _sieht_, und die
eigenen Kosten eines Runs landen erst auf seiner Zeile, wenn er fertig ist.

Die Baseline, die ein Run liest, ist also die Summe der Runs, die bereits fertig
sind, und **gleichzeitige Runs sind füreinander unsichtbar**. Fünfzig Runs, die
gemeinsam gegen eine Organisation starten, der ein Aufruf bis zu ihrem Cap fehlt,
lesen alle dieselbe Baseline unterhalb des Caps, laufen alle weiter und
überschießen um bis zu ihre gemeinsamen Kosten.

Das ist eine Eigenschaft eines Aggregats ohne eine einzelne Zeile zum Sperren —
anders als das Überschießen pro Run und pro Schleife oben, das die Prüfung vor
der Anfrage sehr wohl begrenzt. Deshalb ist das Cap eine Obergrenze auf
**committete** Ausgaben und kein Gate, das gleichzeitige Runs serialisiert.

Ein Deployment, das ein striktes Cap braucht, lässt seine Agents durch eine
einzige Queue laufen statt parallel.

### Evals zahlen aus denselben Budgets { #evals-spend-from-the-same-budgets }

Ein [Eval-Run](evals.md) besteht aus gewöhnlichen Runs. Jeder Fall wird daher
vor jeder Modellanfrage gegen die Limits des Agents und der Organisation
geprüft, und was er ausgibt, zählt gegen beide - ein eigenes Eval-Budget gibt
es nicht. Ein großes Dataset kann deshalb das Monatsbudget aufbrauchen, das der
Produktionsverkehr braucht. Der Start eines Runs zeigt, was jedes Limit nach
der geschätzten Ausgabe behält, und wird abgelehnt, bevor etwas läuft, wenn die
Schätzung höher ist als das, was einem der beiden bleibt; siehe
[Das verbleibende Budget](evals.md#the-budget-it-leaves).

### Ein Run kostet mehr als seine Modellanfragen { #a-run-costs-more-than-its-model-requests }

Eine Wissenssuche bettet die Frage ein, bevor sie danach suchen kann, und dieses
Embedding wird dem Run berechnet, der es angefordert hat. Der Embedding-Dienst
ist prozessweit — er bedient jeden Run und jeden Ingestion-Job zugleich — und
bucht deshalb gegen den Run, der _gerade gemessen wird_, statt ein Budget als
Argument entgegenzunehmen.

Was das Messen zu etwas macht, das eine Oberfläche vergessen kann, und das
Vergessen ist lautlos: keine Exception, keine Warnung, nur ein Run, der weniger
meldet, als er ausgegeben hat, und ein Monat der Organisation, der es nie sieht.
Also gehört der Zähler zum vorbereiteten Run und nicht zur Oberfläche. Einen zu
öffnen ist kein Schritt, von dem eine neue Oberfläche wissen muss, denn es gibt
keinen Weg, einen vorbereiteten Agent ohne ihn auszuführen.

Die Capability [Lange Gespräche](reference/capabilities.md#context-management) ist das
andere. Ihre zusammenfassende Strategie schreibt die Zusammenfassung über einen
Agent, den sie selbst baut, sodass diese Anfrage an keinem Budget-Guard
vorbeikommt; die Capability bucht ihre Kosten gegen denselben Zähler.
_Außerhalb_ des Guards zu liegen hat eine Konsequenz, die man kennen sollte: Die
Ausgabe wird festgehalten statt abgelehnt, sodass eine Kompaktierung, die ein Cap
überschreitet, den Run bei der Anfrage danach stoppt.

### Kosten, die sich nicht messen ließen, sagen das { #a-cost-that-could-not-be-measured-says-so }

Die von Stav berechneten Kosten haben Vorrang vor dem Katalog: `usage.cost`
mit `cost_currency` oder die Header `x-stav-cost` und `x-stav-cost-currency`.
Das gilt für Chat Completions und Responses, auch beim Streaming. USD wird
direkt verwendet; NOK erfordert `STAV_NOK_PER_USD`
([Konfiguration](configuration.md)), vom Betreiber gepflegt. Der Wert gibt NOK
pro Dollar an: `10` würde NOK durch 10 teilen und ist keine Empfehlung für den
aktuellen Kurs. Es wird kein Kurs automatisch abgerufen. Usage bewahrt Betrag,
Währung, Kurs und USD-Kosten; das Ledger markiert die Quelle als vom Anbieter
gemeldet. Ungültige Beträge, nicht unterstützte Währungen oder fehlende
Umrechnung lassen den Katalog-Fallback bestehen.

Ohne verwendbare vom Anbieter gemeldete Kosten gilt: `genai-prices` kennt nicht jedes Modell. Trifft ein Run auf eines, für das es
keinen Eintrag hat, wird diese Anfrage mit null gebucht und der Run als
`cost_is_partial` markiert — die Summe ist um genau das zu niedrig, was diese
Anfragen gekostet haben, und die ehrliche Lesart davon ist eine **Untergrenze**.

Dieses Flag reist inzwischen den ganzen Weg nach unten. Es steht auf der
Run-Zeile, auf der Message-Zeile, die ein Turn schreibt, und auf der Summe, die
eine Unterhaltung meldet; jede Oberfläche, die Geld zeichnet, setzt ein `≥`
davor statt einer Zahl, die sich als exakt liest. Null auf einer Message, die
geschrieben wurde, bevor es die Spalte gab, bedeutet _nicht erfasst_, was nicht
dieselbe Aussage ist wie „exakt" — ein Client markiert nur, was er weiß.

**Jede Oberfläche hält es jetzt fest, und jeder Turn hält seinen eigenen Anteil
fest.** Eine Message, die von einem Channel, der API oder dem Widget geschrieben
wurde, trug bis vor Kurzem gar keine Kosten, sodass sich ein Slack-Thread nicht
aufsummieren ließ. Die geschriebene Zahl ist die _Differenz_ zu dem, was die
früheren Turns dieses Runs bereits beanspruchen, nicht die Zahl der Run-Zeile:
Eine Run-Zeile ist kumulativ, und ein Run, der parkte und fortgesetzt wurde,
schreibt zwei Assistenten-Turns — beide mit der Zeile zu stempeln würde die
geparkte Hälfte doppelt zählen. Die Messages eines Runs summieren sich daher auf
genau das, was der Run als seine Ausgabe angibt.

**Ein Turn, bei dem der Run mittendrin gestoppt wurde, sagt das.** Ein
abgebrochener Run hinterlässt, was der Agent geschrieben hatte, als der Socket
schloss oder `stop` gedrückt wurde, und das liest sich genau wie eine fertige
Antwort — ein Leser nimmt eine abgeschnittene also für alles, was der Agent zu
sagen hatte, und das ausgegebene Geld sieht aus, als hätte es das gekauft. Das
Transkript trägt den Status des Runs pro Turn, und der Chat markiert ihn.

### Wie voll das Context-Window ist { #how-full-the-context-window-is }

Die dritte Obergrenze, und die, die niemand kommen sieht. Ein Budget lehnt mit
einer Nachricht ab, mit der jemand etwas anfangen kann. Ein Workspace lehnt einen
Schreibvorgang ab. Ein **Context-Window** wird vom Provider abgelehnt, mitten in
der Antwort, und der Run scheitert einfach.

Jeder Agent trägt deshalb eine Anzeige — nicht nur einer mit gebundener
Capability [Lange Gespräche](reference/capabilities.md#context-management), denn die
Warnung zählt am meisten für den Agent, der _nicht_ kompaktieren wird. Sie
meldet, wie viele Token die letzte Anfrage eines Turns getragen hat, _nach_ jeder
Kompaktierung: Der Wert fällt, wenn die Kompaktierung wirkt, weil er misst, was
hinausging, und nicht, was die Unterhaltung hält.

Die Zahl ist das `input_tokens` des Providers selbst, keine Schätzung der
Historie. Eine Zeichenzählung kann die Tool-Definitionen nicht sehen, und die
werden bei jeder Anfrage berechnet — Tausende Token bei einem Agent mit Wissen,
einem Sandbox und Delegation, also ein Drittel der echten Zahl, das genau in dem
Moment fehlt, in dem die Zahl zählt.

**Die Zählung wird auf dem Turn gespeichert; der Anteil nicht.**

Wie viel Historie es gibt, überlebt einen Modellwechsel. Welchem Bruchteil eines
Windows das entspricht, nicht — und der Chat lässt jemanden zwischen den Turns
das Modell wechseln.

Eine Historie von 500.000 Token ist die Hälfte eines Modells mit 1M Context und
**390 %** eines Modells mit 128K, und das zweite ist eine Anfrage, die der
Provider rundheraus ablehnt. Ein mit dem Messwert eingefrorener Anteil würde
weiterhin „50 %" lesen.

Also wird der Nenner dort aufgelöst, wo die Auswahl bekannt ist, aus dem eigenen
erfassten Window des Modellprofils und der Preis-Registry dahinter. Wo keines von
beiden etwas sagen kann, wird **überhaupt kein Anteil gezeichnet** — ein
Prozentwert gegen ein angenommenes Window ist eine Schätzung, die als Messung
auftritt.

Dieser Wechsel ist auch das, wofür die Kompaktierung da ist. Ihr Auslöser ist ein
**pro Anfrage aufgelöster Bruchteil** gegen das Modell, an das die Anfrage geht,
sodass eine Historie, die im alten Window bequem Platz hatte, unter dem neuen
gleich beim nächsten Turn kompaktiert wird — bevor die Anfrage hinausgeht, nicht
nachdem der Provider sie abgelehnt hat. Ein Agent ohne gebundene Kompaktierung
hat stattdessen die Anzeige, und sonst nichts.

**Der Auslöser misst, was der Provider gemessen hat.** Er verankert sich an der
jüngsten Antwort, die Provider-Usage trägt — die `input_tokens` jener Anfrage
zählten die Instruktionen, jedes Tool-Schema und jede vorherige Message — und
schätzt nur, was danach kam. Deshalb trägt eine wiederholte Unterhaltung, was
jede Antwort gekostet hat: Ohne Anker zählt der Auslöser Zeichen, und ein echter
Agent las hier 9 Token, wo der Provider 3.859 berechnet hatte. Die Anzeige sagte
77 %; der Auslöser sah nichts zu tun.

**Eine Zusammenfassung wird behalten.** Die Kompaktierung schreibt die Messages
eines Runs um; der Faden zwischen den Turns wird aus dem Transkript neu
aufgebaut, sodass eine Zusammenfassung früher an der Turn-Grenze weggeworfen
wurde und der nächste Turn eine weitere über eine um einen Turn längere Historie
kaufte — zwei aufeinanderfolgende Turns einer echten Unterhaltung bezahlten hier
je eine Zusammenfassung derselben fünf Messages, und die zweite kündigte an, neun
zusammenzufassen. Also wird die kompaktierte Historie in die Unterhaltung
geschrieben, samt ihrer Reichweite, und der nächste Turn startet von ihr und
spielt nur nach, was seither gesagt wurde. Wer den Faden wieder öffnet, findet
dasselbe wie das Modell.

Behalten wird nur eine Zusammenfassung. Die ältesten Messages fallen zu lassen
und Tool-Ergebnisse zu leeren kostet nichts, um es zu wiederholen, und es
aufzuschreiben würde einen Verlust dauerhaft machen, der derzeit bei jedem Turn
neu gegen das Window abgewogen wird.

Eine Einstellung kann nicht funktionieren und sagt das, statt zu laufen. Wenn
allein die Instruktionen und Tool-Schemata über dem Auslöser liegen, kann keine
Zusammenfassung darunter kommen — sie stehen nicht in der Historie, die
zusammenzufassen wäre. Jede Anfrage würde dann eine Zusammenfassung kaufen, die
nichts ändert. Die Kompaktierung wird übersprungen, der Chat sagt warum, und die
Behebung ist Sache des Autors: ein größeres Window oder ein höherer Bruchteil.

Diese Ablehnung ruht auf einer Zahl, die eine _Antwort_ erzeugt, sodass ein Turn
seine eigene nicht messen kann, bevor er entscheiden muss — und ein Chat-Turn ist
meist eine Anfrage. Die Unterhaltung trägt den letzten Messwert, und ein Run
startet von ihm. Der erste Turn eines Fadens hat daher nichts, woran er sich
halten kann, und kompaktiert wie konfiguriert; ab dem zweiten steht die Ablehnung
zur Verfügung. Abgelehnt werden nur die Strategien, die etwas kaufen: Die
ältesten Messages fallen zu lassen und Tool-Ergebnisse zu leeren ruft kein Modell
auf, also laufen sie unabhängig vom Window.

### Delegation gibt das Budget des Parents aus { #delegation-spends-the-parents-budget }

Ein Run kann die ganze Unterhaltung eines anderen Agents enthalten — siehe
[Delegate vs. Inline-Spezialist](concepts.md#delegate-vs-inline-specialist). Ein
Run hat **ein Spend-Ledger**, und jeder Delegate schreibt hinein. Das ist es, was
das Cap des Parents die Ausgaben einer Delegation vor seiner nächsten
Modellanfrage sehen lässt, genau in dem Moment, in dem Delegation vervielfacht,
was ein Turn kosten kann. Jeder Eintrag ist mit der Delegation gestempelt, die
ihn gebucht hat, und so beantwortet ein einziges Ledger trotzdem „was hat
_dieser_ Delegate gekostet" — siehe unten.

Daraus folgt, dass **die Caps, die innerhalb einer Delegation binden, die des
Parents sind**. Das eigene `budget.monthly_usd` eines Delegates wird mitten im
Run des Parents nicht durchgesetzt: Zwei Guards, die ein Ledger messen, würden
jede Anfrage doppelt zählen, und die Obergrenze, auf die es ankommt, ist die des
Runs, den jemand gestartet hat. Das eigene Cap des Delegates gilt weiterhin für
Runs _des Delegates selbst_.

Jeder Delegate bepreist allerdings seine eigenen Anfragen, denn ein Guard
bepreist, was er festhält: Ein Delegate auf Anthropic, gemessen durch einen für
OpenAI gebauten Guard, würde gegen den falschen Katalog bepreist — lautlos, und
meist als unbepreist.

Drei weitere Obergrenzen gibt es, weil ein Budget ein schlechtes Mittel ist, um
ein Fan-out zu stoppen — es merkt es erst, wenn das Geld weg ist. `max_depth`
begrenzt die Verschachtelung, `max_fanout` begrenzt, wie viele Delegationen
zugleich laufen, und das eigene `max_steps` jedes Delegates begrenzt seine
Schleife. Siehe die
[`subagents`-Capability](reference/capabilities.md#delegation).

Zwei dieser drei gehören dem Delegate selbst, und das ist die Linie, die das
Budget nicht überschreitet: `max_steps` wird vom Spec des Delegates gelesen, und
sein `max_depth` deckelt, wie tief _es_ gehen darf, wie viel Raum sein Aufrufer
auch übrig hatte. Ein Cap auf Ausgaben ist ein Cap auf den Run, den jemand
gestartet hat; ein Cap auf Verschachtelung ist eine Entscheidung, die der Autor
des Delegates getroffen hat und seine Prüfer lesen, also kann ein Aufrufer sie
nicht aufweiten.

### Wie ein delegierter Run festgehalten wird { #what-a-delegated-run-is-recorded-as }

Eine Delegation an einen **veröffentlichten** Agent bekommt eine eigene
`agent_runs`-Zeile, die `parent_run_id` und die Task-Id der Delegation trägt. Ein
**Inline-Spezialist** bekommt keine: Er hat keinen Agent, dem eine zuzuordnen
wäre, also sind seine Kosten die _des Runs_, und der Tool-Aufruf im Transkript
ist der Datensatz.

Welchem Run allerdings, ist die Frage, die
[#228](https://github.com/vstorm-co/agenticos/issues/228) beantwortet hat.

- Ein Spezialist direkt unter dem eigenen Agent des Runs wird auf die **oberste
  Zeile** gebucht, die ohnehin das ganze Ledger ist.
- Ein Spezialist unter einem **veröffentlichten Delegate** wird auf die Zeile
  _dieses Delegates_ gebucht, nicht auf die oberste — der Monat des Delegates
  enthält also, was sein Spezialist ausgegeben hat, und das ist der einzige Ort,
  an dem es ehrlich landen könnte.

Jeder Ledger-Eintrag trägt daher zwei Zuordnungen: die Delegation, die ihn
gemacht hat, für das Panel, und die nächstgelegene Agent-Zeile, auf die er
gebucht wird, für den Monat.

Die beiden sind für jede Anfrage gleich, die ein veröffentlichter Delegate auf
eigene Rechnung stellt, und weichen nur unter einem Inline-Spezialisten
voneinander ab — dessen Panel seinen eigenen Anteil behält, während seine
Ausgaben die Zeile seines Vorfahren erreichen.

!!! important "Die Zeile des Parents ist die Autorität; die Zeile eines Kindes ist ihr Anteil daran"

    Ein Delegate gibt in das gemeinsame Ledger aus, und **jeder Eintrag in diesem
    Ledger trägt die Delegation, die ihn gemacht hat**. Die Kosten einer
    Delegation sind die Summe ihrer eigenen Einträge — der Anfragen, die ihr
    eigener Agent gestellt hat, einmal bepreist, durch dieselbe Abfrage, die auch
    die Summe des Runs benutzt. Sie sind in beiden Modi und in jeder Tiefe exakt,
    und sie hängen nicht davon ab, wann die Delegation zufällig abgerechnet
    wurde.

    Genau davon hingen sie einmal ab, und es waren zwei Defekte. Die Zahl war der
    *Zuwachs* der gemeinsamen Summe über die Delegation hinweg, sodass eine
    Delegation im Hintergrund — abgerechnet, wenn sie das nächste Mal abgefragt
    wird, was nach der Antwort des Parents sein kann — alles aufsog, was der
    Parent zwischenzeitlich ausgegeben hatte: Ein Delegate, der 0,01 $ ausgegeben
    hatte, wurde mit 0,51 $ festgehalten, wenn der Parent danach 0,50 $ ausgab.
    Und ein Delegate, der weiter delegiert, hatte die Ausgaben seiner eigenen
    Delegates in seinem Fenster, die deren Zeilen erneut festhalten, sodass seine
    Monatssumme seine Enkel mitzählte.

    Das mit einem Ledger *pro Agent* aufzuteilen bleibt das Design, das zu
    vermeiden ist — genau das hindert das Cap des Parents daran, überhaupt zu
    binden. Ein Ledger, zugeordnet, behält beide Eigenschaften: Das Cap des
    Parents sieht jede Anfrage vor der nächsten, und jede delegierte Zeile sagt,
    was dieser eine Agent ausgegeben hat, seine eigenen Inline-Spezialisten
    eingeschlossen und seine veröffentlichten Delegates ausgeschlossen.

    Die Zeile des Parents bleibt die Autorität für den Run. Ihr `cost_usd` ist das
    ganze Ledger, Delegates eingeschlossen, und das ist es, was der Organisation
    berechnet wird; die Child-Zeilen teilen dasselbe Geld nach Agent auf und
    addieren nie etwas hinzu. `cost_is_partial` gilt ebenfalls je Zeile: Ein
    Parent auf einem Modell, das `genai-prices` nicht kennt, macht die Summe des
    Parents zu einer Untergrenze und sagt nichts über einen Delegate, der auf
    einem bepreisten Modell lief.

    Das `started_at` und `ended_at` einer delegierten Zeile sind die **eigene**
    Spanne der Delegation, abgelesen am Task-Handle, das die Bibliothek stempelt,
    wenn der Delegate startet und wenn er endet — nicht der Moment, in dem die
    Zeile abgerechnet wurde. Von der Abrechnung gelesen, erschien eine Delegation
    im Hintergrund als Run ohne Dauer zur falschen Zeit, einsortiert nach Arbeit,
    die vor ihr fertig war; zwei, die sich tatsächlich überlappten, wurden im
    selben Augenblick festgehalten, ohne dass etwas das sagte. Ein terminales
    Handle mit einem Ende, aber ohne Start — ein Delegate, der abgebrochen ist
    oder scheiterte, bevor er zu laufen begann — hält eine Spanne der Länge null
    an diesem Ende fest, nie ein Null; und wo die Bibliothek ablehnt, bevor
    überhaupt ein Handle existiert — eine unbekannte `chat_trace_id` —, wird keine
    delegierte Zeile geschrieben. Eine Delegation, die auf einer Freigabe geparkt
    hat, spannt sich über jeden Turn, in dem sie lief: Ihr frühester Start wird
    über das Parken hinweg mitgeführt, so wie ihre Kosten (siehe unten), sodass
    die Zeile beginnt, wenn der Delegate zuerst begann, und endet, wenn er
    endgültig endete — nicht beim Fortsetzen, das sie abgerechnet hat. Die beiden
    werden nicht so summiert wie die Kosten; die ehrliche Antwort ist der Start
    des ersten Segments und das Ende des letzten.

**Eine Delegation, die auf einer Freigabe geparkt hat, ist mehr als ein Anteil.**
Ihre Turns liefen in verschiedenen Prozessen gegen verschiedene Ledger, und das
Ledger eines fortgesetzten Turns ist ein frisches Objekt, das nichts von vor dem
Parken hält — was die Child-Zeile festhält, ist also jedes Segment zusammen: Der
geparkte Zustand behält, was die Delegation beim Stoppen gekostet hatte, und der
Turn, der sie beendet, addiert seinen eigenen Anteil. Eine Zeile wird
geschrieben, einmal, von dem Turn, in dem die Delegation endet — ein Delegate,
der zweimal parkte, hinterlässt drei Segmente und eine Zeile.

`cost_is_partial` wird auf dieselbe Weise mitgeführt, und zwar aus einem Grund,
den das Geld nicht teilt: Es gilt jetzt je Zeile statt je Run, sodass ein
Delegate, der _vor_ der Freigabe eine unbepreiste Anfrage stellte und auf einem
bepreisten Modell fortsetzte, sonst auf seiner Zeile exakte Kosten behaupten
würde. Das Flag ist wahr, wenn es für irgendein Segment wahr war.

Das ist es wert, gesagt zu werden, weil der Fehlschlag, den es ersetzt, unsichtbar
war. Die Zeile hielt früher nur das fest, was der Delegate _nach_ dem letzten
Fortsetzen ausgegeben hatte, und das ist in der gewöhnlichen Form — erst die
Arbeit tun, dann um Erlaubnis bitten, auf das Ergebnis hin zu handeln — die kleine
Hälfte. Nichts ging in der Summe daneben, denn das Geld lag die ganze Zeit in der
Zeile des Parents; falsch war jede Zahl, die „was hat dieser Delegate gekostet"
beantwortet.

Die Child-Zeile ist das, was zwei verschiedene Fragen beantwortbar macht, und sie
wollen entgegengesetzte Arithmetik:

| Die Frage | Child-Zeilen |
| --- | --- |
| **Was schuldet die Organisation?** | **ausgeschlossen** - die Zeile des Parents enthält diese Token bereits, beides zu zählen stellt der Organisation also eine Anfrage doppelt in Rechnung |
| **Was hat _dieser Agent_ diesen Monat gekostet?** | **eingeschlossen** - die Zeilen eines Delegates sind der einzige Ort, an dem seine eigenen Ausgaben festgehalten sind, und jede hält die eigenen Anfragen dieses Agents und die seiner Inline-Spezialisten ([#228](https://github.com/vstorm-co/agenticos/issues/228)), aber nicht die seiner veröffentlichten Delegates, die eigene Zeilen haben |

Die zweite ist das, was „der Researcher hat diesen Monat 40 $ gekostet"
beantwortbar macht, und sie ist es, worauf ein Nutzungsbericht je Agent oder ein
Budget-Alert auf diesen Agent auslöst. Die Monatszahl der Organisation trägt
zusätzlich die Ausgaben für Ingestion, die Zahl je Agent nicht: Eine gemeinsame
Wissensdatenbank zu indexieren sind niemandes Agent-Ausgaben.

Eine fehlgeschlagene Child-Zeile wird ebenfalls an die Regel des Parents
gehalten, was `error` sagen darf. Was unter einem Delegate auslöst, ist ein
Modell-Client, dessen Nachricht die URL der fehlschlagenden Anfrage tragen kann —
samt Schlüssel, auf einem eigenen Endpunkt —, also speichern die Zeile und der
Abschlussframe der Delegation denselben kontrollierten Satz wie die Zeile des
Parents, und der eigene Text des Providers geht ins Server-Log. Ein Delegate, den
sein Usage-Limit oder eine Budget-Obergrenze gestoppt hat, behält die Nachricht
des Limits unversehrt: Das ist eine Obergrenze, die ihre Arbeit tut, kein
Fehlschlag, der zu diagnostizieren wäre.

Die Regel reicht auch bis ins Transkript des _Parents_. Wenn ein Agent im
Hintergrund delegiert, fragt er `check_task` und `wait_tasks` nach dem Ausgang
ab, und was diese antworten, wird zu einer Tool-Aufruf-Zeile in der Unterhaltung
— unversehrt gespeichert, denn eine Tool-Rückgabe ist die eigene Antwort des
Tools und nicht etwas, das diese Plattform verfasst hat. Also nennen sie die
Klasse der Exception, statt die Nachricht des Providers zu wiederholen, und das
ist `subagents-pydantic-ai` 0.2.20 und der Grund, warum die Untergrenze dort
liegt.

**Die gefensterte Zahl des Dashboards trägt es ebenfalls.** `GET /stats/usage`
antwortet mit einem `cost`-Block für den Zeitraum, den der Filter gewählt hat,
und dieser Block sind Runs _plus_ Ingestion — dieselbe Arithmetik, mit der das
monatliche Cap gemessen wird — mit `model_usd` und `ingestion_usd` daneben, damit
ein Leser sehen kann, wohin das Geld ging, ohne zu subtrahieren. Bis 0.0.152
meldete er allein die Modellhälfte, was zwei verschiedene Definitionen von Kosten
auf eine Karte brachte: Die Schlagzahl bewegte sich mit dem Zeitraumfilter und
zählte Runs, während die Zeile „seit Monatsbeginn" darunter die ganze Rechnung
zählte, und nichts sagte, dass sie verschiedene Fragen beantworteten. Auf einem
Deployment, das Dokumente indexiert, widersprachen sie sich schlicht.

Bei `scope=own` ist die Ingestion-Hälfte null statt ein Anteil: Ein Dokument wird
von einem Worker indexiert und `ingestion_spend` hält keinen Nutzer fest, einer
Person das Fenster für eine Sammlung zu berechnen, die jemand anderes
synchronisiert hat, hieße also, ihre Ausgaben zu erfinden.

**Jede Abfrage muss sagen, welche der beiden sie beantwortet**, und die erste
Spalte ist die Voreinstellung. Die Zahl seit Monatsbeginn und die Aufschlüsselung
je Agent dahinter schließen Child-Zeilen aus, sodass sie sich auf die Summe
darüber addieren — und die Nutzungs-E-Mail der Organisation meldet dieselbe Summe
statt eine Summe aus einer von ihnen. Nur eine Frage, die _über einen Agent_
gestellt wird, schließt sie ein. Drei dieser fünf Abfragen gingen ohne diese
Unterscheidung live und meldeten je 1,40 $ für 1,00 $ Arbeit; kommt eine neue
hinzu, ist die Voreinstellung die sichere.

#### Die zwei Anbieterfragen brauchen eine dritte Antwort { #the-two-vendor-questions-need-a-third-answer }

**Nach Provider** und **nach Schlüssel** können keine der beiden Spalten
benutzen, und das falsch zu machen ist auf dem Bildschirm unsichtbar.
Child-Zeilen auszuschließen summiert korrekt und ordnet das Geld des Delegates
dann dem Anbieter des _Parents_ zu, weil das der Provider auf der summierten
Zeile ist: Ein Orchestrator auf OpenAI, der 0,40 $ Arbeit an einen Agent auf
Anthropic delegierte, meldete `openai $1.00`und überhaupt keine Anthropic-Zeile.
Sie einzuschließen meldete`openai $1.00` + `anthropic $0.40` — mehr als die
Rechnung.

Diese beiden summieren also die **eigenen** Ausgaben jedes Runs: seine Kosten
abzüglich der Kosten seiner direkten Delegationen. `openai $0.60` +
`anthropic $0.40`, was zugleich die richtige Zuordnung und die richtige Summe
ist. Es verschachtelt sich — einem Delegate, der weiter delegiert hat, werden
seine Enkel dadurch herausgerechnet, einmal — und über jede Zeile summiert kommt
es immer noch auf die Rechnung, weil die Kosten jedes Kindes von seiner eigenen
Zeile addiert und von der seines Parents entfernt werden. Ein Schlüssel
funktioniert genauso, und er zählt mehr: Ein Schlüssel ist das, was jemand
rotiert, wenn eine Rechnung falsch aussieht.

### Das Budget eines Sprachanrufs { #a-voice-calls-budget }

Ein [Sprachanruf](voice.md) ist ein Run, und seine Ausgaben zählen gegen die
Budgets des Agents und der Organisation wie jede andere. Drei Dinge sind anders,
weil sich ein Anruf nicht vor jeder Anfrage prüfen lässt, wie es bei einem
Text-Run geschieht:

- **Bevor er beginnt**, wird die längstmögliche Dauer des Anrufs mal der
  Worst-Case-Minutenpreis des Modells gegen das verbleibende Budget geprüft. Ein
  Anruf, der sich nicht bezahlen ließe, wird abgelehnt, statt begonnen und
  abgeschnitten zu werden.
- **Während er läuft**, wird jede Antwort beim Abschluss auf den Run gebucht,
  und die Ausgaben des Monats werden alle 15 Sekunden neu gelesen, sodass
  andere Runs, die inzwischen ausgeben, gesehen werden. Ein Budget, das mitten
  im Anruf ausgeht, bekommt einen gesprochenen Satz und beendet den Anruf,
  festgehalten als `budget_exceeded`.
- **Seine Länge ist die des Agents.** Ein Anruf dauert höchstens so lange wie
  der längste Anruf des Agents, standardmäßig 10 Minuten und höchstens 50; ein
  eigenes Kontingent an Anrufminuten gibt es nicht, die Budgets sind also die
  einzige Grenze dessen, was Anrufe ausgeben.

Einen Anruf, dessen Prozess gestorben ist, schließt der Voice-Sweep zu den
Kosten, die sein letzter Heartbeat festgehalten hat — anders als einen Text-Run,
dessen Ausgaben mit seinem Prozess sterben.

### Arbeit aus eigener Initiative { #proactive-work }

Nächtliche Vorschlags- und Briefing-Runs werden unter dem veröffentlichten **AgenticOS**-Starter-Agent der
Organisation (Slug `agenticos`) aufgezeichnet, damit jeder eine Zeile, Kosten und
eine Spur hat; der Starter läuft jedoch nicht und wählt das Modell nicht: Jeder
solche Run trägt die [Systemrolle](#system-agents), die die Arbeit erledigt hat.
Das eigene Proaktivitätsmodell und die Limits wählen Sie unter **Organisationen →
Mitglieder → Proaktivität**.

Aktivität zeigt das Symbol und den Filter
**Proaktiv**, die Modellanfragen und ihre Kosten sowie den aufgezeichneten
Austausch einschließlich Tool-Ergebnissen und der strukturierten Endantwort.
Fehlgeschlagene Prüfungen behalten den Austausch, der vor dem Fehler vorlag. Diese
Aufzeichnungen erfordern `runs:view`, wie andere Aktivitätseinträge, und erzeugen
keine Unterhaltungen im Chat. Ältere Runs können nie aufgezeichnete Austausche
nicht wiederherstellen; zu große Aufzeichnungen sagen ausdrücklich, dass sie
gekürzt wurden.

Das Modellfeld verwendet dieselbe Auswahl für Anbieter, Modell und Vault-Schlüssel
wie der Agent Builder. Mit `connections:manage` kann ein Mitglied dort ein
Modellprofil erstellen; ohne diese Berechtigung wählt es ein vorhandenes Profil.
Ein neues Profil wird sofort gespeichert. **Speichern** übernimmt die Auswahl
für die nächtlichen Vorschläge. Das Briefing listet vorhandene Funde ohne Modellaufruf. Beobachtungen durch ausgewählte
Agenten teilen das Limit pro Person.

Die Plattform kann ein wenig aus eigener Initiative tun, und tut nichts davon,
bis eine Organisation es einschaltet: die Karte **Proaktivität** auf ihrer
Mitgliederseite (`budgets:manage`). Eingeschaltet liest ein Hintergrundjob die
letzten Anfragen jeder Person an den Tagen und zu der Stunde, die die
Organisation gewählt hat - in der Zeitzone jeder Person oder eine Stunde vor der
Arbeitszeit, die ihr Profil angibt - und schlägt eine Routine für eine Anfrage
vor, die sie immer wiederholt, ein Profil oder die ersten Nachrichten, die ihr
leerer Chat anbietet ([Konsole](console.md#chat)).

Jeder ist ein Modellaufruf,
auf dem Modell, das die Organisation beim Einschalten gewählt hat (es gibt keinen
Standard: `proactive_model_profile_id` ist Pflicht), in der Run-Historie als Run
der Oberfläche **proactive** festgehalten und wie jeder andere auf das Budget der
Organisation angerechnet. Eine Person kann sich unter **Einstellungen →
Assistent** ausnehmen.

Der nächtliche Blick wird 20 Stunden nach seinem eigenen letzten Versuch
fällig. Beobachtungen und Speicherwartung verschieben diesen Zeitpunkt nicht.
Beobachtungen zählen weiterhin zum persönlichen Proaktivitätsbudget.

Ein erfolgreicher Blick speichert die Person und einen Hash jedes gelesenen
Prompts, ohne den Text zu kopieren. Das Abschließen einer bereits gelesenen
Genehmigungs- oder Fragepause löst daher keinen weiteren bezahlten Modellaufruf
aus. Ein zurückgestellter Prompt bleibt ungelesen, bis ein späterer Blick ihn
berücksichtigt; geänderter Text ist wieder neu. Fehlgeschlagene Modellaufrufe
markieren keine Prompts als gelesen.

Nach dem Upgrade verschieben alte Hintergrund-Runs ohne Rollenzuordnung den
nächsten Blick nicht. Bestehende Nachrichten haben keinen Lesenachweis, sodass
geeignete aktuelle Prompts einmal nachgeholt werden können. Die üblichen
Schalter und Budgets gelten weiterhin.

Der nächtliche Job liest nur einen Prompt, der den Eingabe-Guardrail des Agents
passiert hat, an den er ging, denn das Proaktivitätsmodell hat keine eigenen
[Guardrails](reference/capabilities.md#guardrails):

- Ein Prompt, den ein Guardrail abgelehnt hat, bleibt draußen.
- Ein Prompt, den kein Run angenommen hat, bleibt draußen: einer an einen
  archivierten Agent, einer, der abgelehnt wurde, bevor sein Run begann, und ein
  Chat-Prompt, der an einen Workflow geht.
- Ein Prompt, dessen Run noch läuft, wartet. Der Job liest ihn in einer späteren
  Nacht, wenn der Run beendet ist.
- Stoppt ein Run vor seiner ersten Model-Anfrage, zum Beispiel am Budget, lässt
  der Job den Eingabe-Guardrail dieses Runs über den Prompt laufen und lässt aus,
  was er blockiert. Gibt es die Agent-Version nicht mehr, bleibt der Prompt draußen.
- Hat die Aufbewahrung den Run gelöscht, liest der Job den Prompt nur, wenn eine
  Antwort eines Models auf ihn folgt. Ein Prompt von vor Version 1.36.0 wird
  genauso gelesen, also bleibt die Ablehnung eines Eingabe-Guardrails draußen,
  denn sie hat keine Antwort hinterlassen. Ein gesprochener Turn zeichnet kein
  Model auf, also bleibt er draußen, sobald sein Run weg ist.

Routinen-Prompts werden mit der festgelegten Agent-Version, der Version der
benannten Umgebung oder der aktuellen Agent-Version geprüft, wie beim Start
der Routine. Ein fehlendes Ziel oder eine unlesbare Spec wird ausgelassen, ebenso
eine Routine, deren letzter Lauf abgelehnt wurde. Eine bereits gestartete Routine,
deren letzter Run durch die Aufbewahrung gelöscht wurde, wird ebenfalls ausgelassen:
Ein unbekanntes Ergebnis kann eine Ablehnung nicht aufheben.

Ein Prompt, den ein Guardrail geschwärzt hat, passiert, und der Job liest ihn so,
wie er gespeichert ist, also ungeschwärzt
([#2655](https://github.com/vstorm-co/agenticos/issues/2655)).

Darüber hinaus begrenzt das **Limit für Arbeit aus eigener Initiative** der
Organisation die Runs jeder Person in einem Kalendermonat - standardmäßig 1 $,
auf derselben Karte eingestellt (`proactive_limit_usd`). Eine Person, bei der
nichts Neues passiert ist, kostet nichts: Der Job ruft das Modell nur, wenn es
etwas zu lesen gibt.

**Beobachtungen** laufen durch den ausgewählten veröffentlichten Agenten,
mit demselben Runner wie Chat und Routinen sowie seinen eigenen Anweisungen und
seinem Modell. Sie werden getrennt eingeschaltet (`proactive_watches_enabled`)
und teilen das Proaktivitätsbudget je Person, nicht das Briefing-Modell.
Erstelle sie unter **Routinen**, neben den Zeitplänen eines Agenten mit
**Beobachten und informieren**, neben einem persönlichen Konto oder im privaten
Chat. Wähle Agent, Konto, Intervall und optionales Ende. Die Beobachtung folgt
Veröffentlichungen, einer benannten Umgebung oder einer unveränderlichen
Version. Die Organisation bestimmt das Mindestintervall
(`proactive_watch_min_minutes`).

Die Veröffentlichung muss diesen persönlichen Katalogdienst bereits einbinden.
Eine Beobachtung aktiviert keine fehlende Bindung und ersetzt keinen Agenten.
Eigene Server ohne persönliche Katalogbindung werden hier nicht unterstützt.
Die Erstellung genehmigt das konkrete Konto und seinen Endpunkt, die gemeinsame
Teilmenge der Tool-Listen von Konto und Agent sowie die derzeit angehängten
Wissensbasen und Kontextdateien. Spätere Veröffentlichungen dürfen Zugriff
entfernen, aber keine Konten, Tools oder Quellen hinzufügen. **Aktuelle Quellen
erneut genehmigen** erneuert diese Zustimmung ausdrücklich. Ein neues primäres
Konto ersetzt das genehmigte Konto nicht.

Jede Prüfung ist ein gewöhnlicher **proactive**-Run des tatsächlichen Agenten
und seiner Version. Quelltools brauchen `readOnlyHint`. Verfügbar sind das
genehmigte Konto, Wissen/Kontext, Guardrails, Planung und Systemerinnerungen;
Web, Sandbox, ausführbare Skills, Speicherschreibzugriffe und Delegation sind
nicht verfügbar. Erforderliche interaktive Genehmigungen verhindern die
Einrichtung und werden nicht umgangen. Mitgliedschaft, Ausführungsrecht,
Quellzustimmung, Pause und Ablauf werden vor Tools und Ergebnisspeicherung
nochmals geprüft.

Neue Funde behalten ihre stabile Beobachtungs-/Element-ID im persönlichen
Posteingang und erscheinen im Quellchat; außerhalb des Chats erstellte
Beobachtungen verwenden den primären Chat. Nur ausdrücklich gewünschte dringende
Funde senden zusätzlich eine sofortige Benachrichtigung. Im Standardmodus
**Nur bei relevanten Änderungen** gibt es ohne Änderung keine Chatnachricht
oder Abschlussbenachrichtigung. Bei fehlendem Chatzugriff
bleibt der Fund im Posteingang und `delivery_unavailable` wird aufgezeichnet.

Der letzte Versuch ist von der letzten erfolgreichen Prüfung getrennt. Eine
Modellantwort ohne erfolgreichen Quellzugriff oder mit ungelöstem Lesefehler ist
`not_completed`. Nicht verfügbare Quellen und entzogener Zugriff sind explizit;
`budget_paused` bedeutet verbrauchtes Budget. Diese Fehler verschieben den
Erfolgszeitpunkt nicht. Unerwartete Fehler lassen den Job weiterhin fehlschlagen.

`0261_watch_attempt_status.py` beginnt die Versuchshistorie ohne erfundene ältere
Erfolge. `0262_watch_agent_sources.py` erhält IDs, Funde, Intervall und
Aktivierungszustand, erfindet aber keine Agentenzuordnung. Bestehende
Beobachtungen zeigen `needs_agent` und rufen kein Modell auf, bis ihr Eigentümer
einen veröffentlichten Agenten und seine Quellen bestätigt. Das Löschen einer
Beobachtung entfernt Ursprung und Quellenzustimmung eines laufenden oder
wartenden Runs nicht.

Erstellung und Änderung erfordern die eigene angemeldete Sitzung oder den
vertrauenswürdigen privaten menschlichen Chat. Gewöhnliche API-Schlüssel,
Publisher-Ersatzkontext, Hintergrundroutinen und Identitätsübernahme können diesen
Dauerzugriff nicht erteilen. Die Plattform-MCP-Tools `list_watches`, `create_watch`,
`update_watch` und `delete_watch` verwenden denselben Dienst und dieselben IDs.
Vollständige private Chat-Aufträge erstellen oder ändern sofort; Veröffentlichung,
Einladungen, Löschen und sofortiges Ausführen behalten ihre Genehmigungsregeln.

Eine Beobachtung kann **Nach jeder Prüfung, auch ohne Änderungen** wählen
(`report_mode: every_check`). Jede erfolgreiche Prüfung liefert dann den
vereinbarten Bericht in den Quellchat, auch ohne neue Funde, und verwendet das
gemeinsame Benachrichtigungssystem. Fehlgeschlagene oder nicht verfügbare
Lesezugriffe werden nie als unveränderter Stand gemeldet. Die Aktivierungszeit
bleibt im Job; der Berichtsmarker wird mit den Ergebnissen gespeichert. Eine
Wiederholung derselben abgeschlossenen Aktivierung sendet keinen zweiten Bericht
und bezahlt keine zweite Prüfung. Ein weiterer Klassifikationsaufruf entfällt.

Zeitpläne und Beobachtungen erben deine Benachrichtigungseinstellungen.
**Benachrichtigungen für diese Routine** erlaubt beliebige Kombinationen aus
E-Mail, Geräte-Push, Slack, Telegram, Mattermost, Microsoft Teams und SMS oder nur
die App. `notification_channels: null` erbt; eine leere Liste verhindert externe
Zustellung. Persönlich ausgeschaltete Kanäle bleiben aus. Wartende Sendungen
prüfen die aktuelle Richtlinie; Pause oder Löschen stoppt sie. Verpflichtende
Sicherheits-/Konfigurationshinweise bleiben verpflichtend. Bereits vom Anbieter
angenommene Nachrichten lassen sich nicht zurückrufen.

Wähle unter **Einstellungen → Benachrichtigungen** ausdrücklich einen verknüpften
privaten Chat je Plattform für die aktuelle Organisation. Eine Kontoverknüpfung
allein aktiviert nichts. Gruppenchats, fremde Konten und Bots anderer Organisationen
sind ausgeschlossen. E-Mail und Push behalten ihre Einrichtung. Die Chatauswahl
bei den Ereigniseinstellungen bestimmt, welche Plattform du gerade bearbeitest.

Ein Ziel bindet die konkrete Kontoverknüpfung und Bot-Autorisierung. Entknüpfen,
erneutes Verknüpfen oder ein anderer Bot-Token/Server erfordern eine neue Auswahl.
Wartende Sendungen wechseln nie automatisch das Ziel. Vor dem Senden werden
Mitgliedschaft, Kontostatus, Bot-Richtlinie, SMS-Verknüpfungsfrist, Einstellungen,
Ressourcenzugriff und gemeinsamer Fallstatus erneut geprüft. Ohne Entscheidungsrecht
enthält ein Genehmigungshinweis nur die Tatsache des Wartens, keinen Entscheidungslink.

Chatplattformen bieten keinen gemeinsamen idempotenten Sendevertrag. Ein
unbestätigter Versand erhält einen persistenten Versuch: Timeout, Anbieterfehler
oder Worker-Ausfall bleiben als sichtbarer Fehler erhalten. Ein Slack-HTTP-429,
der die Anfrage ausdrücklich ablehnt, erlaubt nach `Retry-After` einen erneuten
Versuch, höchstens fünf Claims insgesamt. Jeder Versand löscht zunächst die
Retry-Freigabe; ein Absturz dieses Versuchs wiederholt ihn nicht. Das Zurückstellen
entwertet das alte Übergabe-Token. Andere Chatfehler sind endgültig. E-Mail und
Push behalten ihre Wiederholungsregeln.

Slack-Hinweise nutzen begrenzte Klartextblöcke und **Open in AgenticOS** zum selben
autorisierten Fall in der Konsole. Namen und Inhalte erzeugen keine Erwähnungen
oder automatischen Vorschauen. Ohne Entscheidungsrecht bleibt nur der Wartehinweis
ohne Entscheidungslink. Zustellung bezeichnet Plattformannahme; Bestätigung bleibt
eine ausdrückliche Entscheidung im gemeinsamen Fall. Migration
`0273_notification_safe_retries.py` gewährt bestehenden Zeilen keine Retry-Freigabe.
Ein Downgrade stellt einen Chatversuch wieder her.

Migration `0271_notification_routes.py` ergänzt Ziele und Autorisierungsidentitäten.
Leere die Worker und aktualisiere API/Worker gemeinsam, bevor du neue Kanäle wählst.
Bestehende Konten bleiben aus. Ein Downgrade entfernt Chatziele, deren Versandhistorie
und Einstellungen und begrenzt Routineauswahlen auf E-Mail/Push; externe Sendungen
bleiben bestehen. `0263_routine_notification_policy.py` führte die ursprünglichen
Routineeinstellungen ein.

### Budgetpausen { #routine-budget-pauses }

Erreicht eine Routine ihr eigenes Monatslimit, erhält ihre besitzende Person
eine Budgetmeldung je Zeitraum und konfiguriertem Limit. Eine Beobachtung nennt
das gemeinsame Budget für proaktive Arbeit dieser Person und verweist zur
Prüfung an die Administration. Die Prüfung stoppt vor dem Öffnen eines Laufs
oder einem Modellaufruf; die Meldung nennt die Monatsausgaben. Die bestehenden
Budgetpräferenzen und die Kanalgrenzen der Routine gelten weiterhin.

Die Meldung öffnet einen bearbeitbaren Fall für diese Aufgabe und diesen Monat.
Weitere Prüfungen verwenden denselben Fall. Bestätigen oder Stummschalten
storniert ausstehende Zustellungen. Hat die nächste Aktivierung wieder Budget
oder wird die Aufgabe entfernt, werden ihre offenen Budgetfälle erledigt und
ausstehende Zustellungen storniert. Ein geändertes Limit erlaubt eine neue
Meldung bei dessen Erreichen; ein stummgeschalteter Fall bleibt im Monat stumm.

### Aktionsfreigaben { #routine-action-consent }

Eine unbeaufsichtigte Routine fragt vor Fähigkeitsaktionen, lokalen Dateiänderungen
und Delegation. Externe MCP-Aufrufe fragen, sofern ihr Server sie nicht als nur
lesend kennzeichnet; eine strengere Bestätigungspolitik der Verbindung gilt weiter.
Die Vorgaben interaktiver Agenten bleiben gleich. Der Codemodus ist für Routinen
ausgeschaltet, damit Sammelskripte keine Einzelkontrollen umgehen.

Schreibzugriffe auf AgenticOS Virtual Tables laufen innerhalb ihrer aktuellen Tabellen- und Operationsfreigaben ohne Ausnahme für die Routine. Eine ausdrücklich konfigurierte Freigaberichtlinie des Agenten gilt weiterhin für diese internen Schreibzugriffe.

Die erstellende Person öffnet **Aktionsfreigaben** ihrer Routine und wählt Fähigkeitsaktionen sowie einzelne MCP-Aktionen eines benannten Kontos. Das erfordert `approvals:decide` und den Organisationsschalter `chat_may_waive_approvals`. API-Schlüssel, Agenten, übernommene Identitäten und Administratoren für andere Personen dürfen nicht zustimmen. Ausdrücklich vom Agenten oder einer MCP-Bindung geforderte Bestätigungen bleiben bestehen.

Eine MCP-Ausnahme benennt das konkrete Konto, den Adresshash, die Autorisierungsgeneration und den ursprünglichen Aktionsnamen. E-Mail-Versand erlaubt weder Löschen noch Schreibzugriffe auf ein anderes Konto. Persönliche Konten stehen nur Routinen zur Verfügung, die als ihre erstellende Person handeln. Die Auswahl verwendet die gespeicherte Werkzeugliste; prüfe die Verbindung oder bitte deren Administration, wenn sie fehlt. Alle Freigaben lassen sich auch ohne funktionierende Werkzeugliste widerrufen.

Beim Speichern müssen Version und Konfiguration noch der menschlichen Prüfung
entsprechen. Eine Freigabe nennt die unveränderliche veröffentlichte Version, stabile
Fähigkeits- und Werkzeugkennungen sowie die Routinenkonfiguration. Geänderte Arbeit
oder Ziele machen sie ungültig. Jeder Lauf behält seine zugelassenen Freigabe-IDs;
eine spätere Zustimmung gibt keinen geparkten Lauf frei, und Widerruf mit erneuter
Zustimmung erzeugt eine neue Identität. Mitgliedschaft, Rechte, Organisationspolitik
und Zustimmung werden vor jedem Werkzeug erneut geprüft. Jeder freigegebene Aufruf
zeichnet eine genehmigte Zeile mit zustimmender Person und Argumenten auf.
Delegierte brauchen eigene Entscheidungen und erben keine Ausnahme des Elternlaufs.

Eine dauerhafte Aktionsfreigabe speichert auch die aktuellen Revisionen der gebundenen Skills, Skill-Ressourcen, Kontextdateien und Projektanweisungen. Jede Bearbeitung macht die Ausnahme ungültig, selbst wenn die veröffentlichte Agentenversion gleich bleibt oder der alte Text später wiederhergestellt wird. Prüfen Sie die Aktionen erneut, um eine neue Ausnahme zu erteilen. Bestehende Ausnahmen ohne Revisionsfingerabdruck gelten nur für Agenten ohne solche veränderlichen Anweisungsquellen weiter.

`0267_routine_action_consents.py` erzeugt keine Ausnahmen für bestehende Routinen.
Eine bisher unbeaufsichtigte Aktion kann nun auf Bestätigung warten. Alte Worker
abwickeln lassen und API sowie Worker gemeinsam aktualisieren, bevor Aufgaben
weiterlaufen; ein alter Worker erzwingt diese Kontrolle nicht. Ein Downgrade
entfernt gespeicherte Ausnahmen; Bestätigungsprotokolle und bereits ausgeführte
Wirkungen bleiben bestehen.

### Antworten fortgesetzter Routinen { #resumed-routine-answers }

Eine nach Bestätigung oder beantworteter Frage abgeschlossene Routine stellt ihre
Konsolenantwort in derselben Transaktion wie den Endzustand zu. Der gespeicherte
Auftrag enthält fertigen Text und Kennungen; ein Neustart benötigt keinen neuen
Modellaufruf. Zustellstatus, Chatnachricht und Abschlussmeldung werden gemeinsam
gespeichert. Gleichzeitige Versuche und Wiederholungen hängen keine zweite Antwort an. Ein
autorisiertes Konversationsereignis aktualisiert den offenen Quellchat nach dem
Commit, ohne ihn zurückzusetzen; ältere Abonnenten behalten ihren Ereigniswortschatz.

Der Worker prüft aktuellen Routinenzustand, Frist, erstellende Person und Ziel.
Chat-Zustellung prüft aktive Mitgliedschaft, Ausführungsrecht am Agenten und
aktuelles Schreibrecht im privaten Chat erneut. Archivierte Chats sind für neue
Nachrichten geschlossen. Bei unerreichbarem Ziel bleibt das Ergebnis in Aktivität
und die Zustellung wird als nicht verfügbar vermerkt. Datenbankfehler lassen den
Auftrag für bis zu drei Versuche offen; Administration → Hintergrundaufgaben zeigt
eine ausgeschöpfte Zustellung. Die Abschlussmeldung verlinkt den Chat nach
erfolgreicher Zustellung. Die Fortsetzung in externen Kanälen bleibt eigene Abnahmearbeit.

`0268_routine_answer_delivery.py` spielt keine historischen Ergebnisse nach.
API und Worker gemeinsam aktualisieren. Vor einem Downgrade ausstehende Aufträge
`deliver-routine-answer` abwickeln oder abbrechen, da er gespeicherte Übergaben
entfernt; bereits zugestellte Nachrichten bleiben erhalten.

### Störungsfälle { #routine-failure-incidents }

Ein fehlgeschlagener Routinenlauf, eine unzugängliche Beobachtungsquelle oder
ein fehlender Zielchat öffnet einen Fall für die besitzende Person. Weitere
Fehler aktualisieren dessen letzte Ursache und Lauf ohne erneuten Alarm.
Bestätigen lässt ihn bearbeitbar und still; Stummschalten gilt für diese Ursache
auch nach einer Erholung. Ein neuer Fehler nach bestätigter Erholung öffnet
denselben Fall erneut und sendet eine neue Meldung.

Der gemeinsame Runner meldet erste, fortgesetzte und nach einem Prozessabsturz
beendete Läufe. Unter terminalen Ergebnissen bestimmt nur der zuletzt gestartete
Hauptlauf den Zustand einer Routine; delegierte Unterläufe und verspätete
Abschlüsse älterer Läufe überschreiben ihn nicht. Eine Beobachtung erholt
sich erst nach einem bestätigten Quellenabruf; ein abgeschlossener Modelllauf
allein genügt nicht. Verspätete Prüfungen erhalten den neuesten Versuch und den
letzten erfolgreichen Abruf unabhängig voneinander. Erkannter Ablauf oder Entfernung
erledigt offene Blockaden und storniert Zustellungen. Pausierte, abgelaufene oder
gelöschte Routinen schreiben keine fertige Antwort in ihren Zielchat.

Beobachtungs- und Erholungszeiten schützen Entscheidungen vor Callback-Retries
und älteren Ergebnissen. Scheitert das Speichern einer Benachrichtigung, bleibt der neue Vorfall
erneut meldbar. Agenten- und Lauflimits teilen den Vorfall der besitzenden Person ohne eine
zweite Fehlermail. Organisations- und Abteilungsbudgets bleiben unabhängige
Finanzmeldungen, auch wenn die besitzende Person administriert. Ihr Routinenfall
bleibt in der App, wenn die Finanzmeldung sie bereits informiert. Die
Budgetpräferenzen gelten weiterhin.

`0266_routine_activation_clock.py` ergänzt die Zeiten ohne erfundene historische
Versuche. Vorhandene Fall-IDs für Beobachtungsfehler bleiben erhalten. API und
Worker werden gemeinsam aktualisiert: Der Zustellungsworker muss die neuen
Budgetmetadaten verstehen. Ein Downgrade entfernt die Zeiten, erhält aber Fälle,
Benachrichtigungen und Entscheidungen.

### Systemagenten { #system-agents }

Manche Arbeit gehört der Plattform selbst und braucht dennoch ein Modell. Eine
**Systemrolle** gibt ihr einen Eigentümer, den ein Betreiber sehen kann: eine
Konfiguration je Organisation und Rolle, nie eine Agent-Zeile, nie eine je Job und
nie dem Starter-Agent oder einem Agent zugeschrieben, dessen Anweisungen nicht
liefen (#2575). **Organisationen → KI und Dienste → Hintergrunddienste** (`budgets:manage`)
listet jede Rolle mit ihrem Modell, den Ausgaben dieses Monats und ihren letzten
Ausführungen.

| Rolle | Was sie tut | Konfiguriert |
| --- | --- | --- |
| `daily_briefing` | Der nächtliche Blick auf die Woche jeder Person und das daraus gebaute Briefing | Im Tab Proaktivität: dasselbe Modell und Limit, hier nur lesbar |
| `memory_maintenance` | Liest eine abgeschlossene zugelassene Runde einmal und hält die dauerhaften Fakten, Vorlieben und Entscheidungen darin als [Claims](reference/capabilities.md#memory-claims) fest | Hier: ein Schalter, ein Modell und ein eigenes Monatsbudget |

Die Gedächtnispflege benötigt den AgenticOS-Starter nicht. Ihr Run behält den Ursprungsagenten als Speicherreferenz und wird als `memory_maintenance` abgerechnet; ausgeführt werden nur die Extraktionsanweisungen der Plattform auf dem für die Rolle gewählten Modell. Entfernen oder Zurückziehen des Starters deaktiviert die Extraktion anderer veröffentlichter Agenten nicht.

Eine Rolle wird ausgeschaltet, nicht gelöscht. Die **Gedächtnispflege** benötigt
ein ausdrücklich gewähltes Modell und beachtet ihr Monatsbudget sowie die Obergrenze
der Organisation. Sie liest abgeschlossene Gesprächsrunden nur, wenn der Runner eine
reale Person mit Schreibrecht zugelassen hat: private Chats, verknüpfte Kanalnachrichten
und eigene Zeitplan- oder Workflow-Aufgaben. Embeds, API-Schlüssel, Identitätsübernahmen,
Ereignisse von Außenstehenden und Delegationen sind ausgeschlossen.

Der Auftrag bewahrt den ursprünglichen Empfängerkreis. Private Runden schreiben in das
Profil der Person oder ihre Beziehung zum Agenten; Gruppenrunden ausschließlich in den
Quellraum, auch bei allgemeinen Fakten. Nach der Modellantwort werden Mitgliedschaft
und Löschungen unter der Schreibsperre erneut geprüft. Ältere Aufträge ohne Empfängerdaten
bleiben auf private Web-Chats beschränkt. Die Pflege teilt und korrigiert keine Claims.
Deterministische Konsolidierung benötigt weder Modell noch Rolle.

### Was die Run-Historie zeigt { #what-run-history-shows }

**`GET /runs` listet nur Runs der obersten Ebene, und sein `total` zählt diese.**
Dieselbe Voreinstellung wie die Monatssumme der Organisation, und aus demselben
Grund: Ineinander verschränkt lassen sich die beiden Arten von Zeile nicht in
einer Kostenspalte lesen. Ein Fan-out aus drei Delegationen ist ein Run, der auf
der Seite 1,00 $ kostet und auf der Rechnung 1,00 $; zusammen gelistet waren es
vier Zeilen mit 1,00 $ + 0,40 $ + 0,40 $ + 0,40 $ neben einer Zahl seit
Monatsbeginn von 1,00 $, und beide Hälften hatten über verschiedene Fragen recht.

Die Liste nimmt dieselbe zweiseitige Arithmetik wie die Summen oben, aus
demselben Grund — sodass eine Oberfläche, die auf einen Agent eingeengt ist,
zeigt, was _dieser Agent_ getan hat, die Arbeit seiner Delegates eingeschlossen:

| Frage | Antwort |
| --- | --- |
| `GET /runs` | Runs, die jemand gestartet hat. `parent_run_id IS NULL` |
| `GET /runs?agent_id=<id>&include_delegations=true` | Die eigene Historie eines Agents. Das fragen das Panel Recent runs im Builder und `?agent=` in **Aktivität** (Aktivität), denn die Zeilen eines Delegates sind der einzige Datensatz darüber, was er selbst getan hat |
| `GET /runs?parent_run_id=<id>` | Was dieser Run delegiert hat — die Abfrage, für die `agent_runs_parent_run_id_idx` existiert. Hat Vorrang vor `include_delegations` |
| `GET /runs/<id>` | Ein Run, delegiert oder nicht. Wo ein Link aus einem Transkript landet |
| `GET /runs/<id>/transcript` | Die Turns dieses Runs, der Reihe nach - was eine Run-Detailansicht als Schritte rendert. Autorisiert, nicht besessen (unten) |

**Einen Run zu lesen ist autorisiert, nicht besessen.**

Ein Kollege, der `runs:view` hält, liest einen Run, den jemand anderes gestartet
hat. Die Autorität über einen Run liegt bei der Organisation, denn ein Run ist
das, was der Organisation berechnet wird und wofür sie zur Verantwortung gezogen
wird — und nicht Privateigentum dessen, der auf Los gedrückt hat.

Die Entscheidung liegt also im Service statt in einem Route-Gate: Er löst den Run
zuerst gegen die Organisation des Aufrufers auf und prüft dann `runs:view`.

Drei Konsequenzen:

- Ein Run in **einem anderen Tenant liest sich als abwesend** — derselbe 404, mit
  dem eine Id antwortet, die es nie gab, bis in den Body hinein —, sodass sich
  über die Antwort nicht herausfinden lässt, dass ein Run existiert.
- Ein Run, der **ohne Unterhaltung** lief (ein API-Aufruf, der keine
  `conversation_id` übergab), hat kein Transkript zu lesen und sagt das mit einer
  `conversation_id` von null statt mit einer leeren Liste, die sich als „er hat
  nichts getan" läse.
- Nichts davon weitet `GET /conversations/{id}/messages`, was **auf den Besitzer
  beschränkt** bleibt. Dass das Transkript eines Runs für einen Kollegen lesbar
  ist, darf den privaten Faden, in dem es steckt, nicht ebenfalls lesbar machen.

**Jeder Turn, den das Transkript ausliefert, trägt die Bewertungen, die Menschen
dazu hinterlassen haben** — den eigenen Daumen des lesenden Aufrufers, die Likes
und Dislikes der Organisation und den Kommentar der jüngsten negativen Bewertung.

Eine gewöhnliche Message-Zeile hält nichts davon, also werden sie in einem Zug
aus `message_ratings` gelesen und an die Turns geheftet. Ein Turn, den niemand
bewertet hat, trägt sie leer und liest sich genau wie eine gewöhnliche Message.

Das ist es, was die Run-Detailansicht die negativ bewerteten Antworten und die
dazu hinterlassenen Worte zeigen lässt — die Unterhaltungen hinter der
Qualitätszahl des Dashboards (#209) —, gelesen dort, wo der Run gelesen wird,
statt nur im Bewertungsexport für App-Admins.

Gezeigt wird der Kommentar einer **negativen** Bewertung, nie einer positiven,
und der jüngste, wenn ein Turn mehr als einen Einwand auf sich zog.

### Was die Aggregate des Dashboards zeigen { #what-the-dashboards-aggregates-show }

`GET /stats/usage` nimmt dieselben zwei Seiten und dieselbe Voreinstellung. Die
zusammengesetzte Antwort ist die Frage der Organisation, also zählt jeder Block
darin nur Zeilen der obersten Ebene: die Kosten des Zeitraums und ihre
Aufteilung nach Provider (die doppelte Rechnung oben), aber auch die Run-Summe,
die Tagesreihe, die Aufteilung nach Ausgängen, die Oberflächen, die
Latenz-Perzentile, die Zahl der aktiven Personen und die Tabelle je Person. Über
die Kosten hinaus _kopiert_ eine delegierte Zeile `user_id` und `surface` ihres
Parents, sie zu zählen würde also zusätzlich eine zweite Person und eine zweite
Ankunft auf einem Channel erfinden, den jemand einmal benutzt hat.

Zwei Aggregate nehmen die andere Seite, und beide werden über einen Agent
gefragt:

| Frage | Child-Zeilen |
| --- | --- |
| `by_agent` — die Adoption-Karte | **eingeschlossen.** Ausgeschlossen hat ein Agent, der vierhundertmal am Tag als jemandes Delegate läuft, keine Zeile, und die Karte bezeichnet jeden veröffentlichten Agent ohne Zeile als vergessen und bietet an, ihn zu archivieren. Ihre Balken können daher die Run-Summe daneben übersteigen; nichts summiert sie |
| `?group_by=version` — die Karte zum Versionsvergleich | **eingeschlossen.** Ein Spezialist, der nur je als Delegate ausgeführt wird, hätte sonst nichts, was sich über seine Versionen vergleichen ließe |

Die Invariante, die so oder so überlebt: Die Segmente des Ausgangs-Donuts
summieren sich weiterhin auf `total_runs`, und sein Segment `awaiting_approval`
zählt weiterhin dieselben geparkten Runs wie die Freigabe-Karte, denn diese drei
kommen von derselben Seite des Schalters.

Die eine Abfrage ganz ohne Delegationsfilter ist die Zahl der Runs des
Aufrufers, die auf einer Entscheidung geparkt sind. Ein geparktes Kind ist ein
steckengebliebener Parent, und diese Karte beantwortet „warum wird mein Agent
nicht fertig"; heute ändert das nichts, denn eine Delegation wird bereits fertig
in die Datenbank geschrieben und parkt daher nie.

Die letzten beiden sind `?run=<id>` auf der Seite **Aktivität** (Aktivität): ein Run, die
Delegationen darunter jeweils mit der Task-Id abgezeichnet, die ihre
`subagent_*`-Frames trugen, und ein Link hinauf zu dem Run, dem eine Delegation
berechnet wurde. Ein Delegationspanel in einem Chat verlinkt mit der `run_id`
dorthin, die sein terminaler Frame trägt — deshalb trägt der Frame eine.
Delegierte Zeilen in der Tabelle der obersten Ebene zu verschachteln wird hier
bewusst _nicht_ getan; ein Tabellen-Primitiv für das ganze Produkt ist
[separat vorgeschlagen](https://github.com/vstorm-co/agenticos/issues/139), und
die Verschachtelung gehört dorthin und nicht in eine maßgeschneiderte
Run-Tabelle.

### Was der Kostenbildschirm zeigt { #what-the-cost-screen-shows }

`GET /spend` nimmt sein Fenster auf zwei Wegen, weil die Seite nach beiden Arten
fragt: `days` für die Voreinstellungen _letzte N Tage_ und `from`/`to` für
_diesen Monat_, _letzten Monat_ und einen Kalenderbereich. `from` gewinnt, wenn
beide ankommen — ein expliziter Bereich ist eine speziellere Anfrage als eine
Voreinstellung, die niemand geändert hat —, und `period_days` kommt in diesem
Fall als null zurück, statt eine Zahl zu wiederholen, der der Bereich
widerspricht.

**Jedes Panel auf dem Bildschirm liest dasselbe Fenster.** Die Zeilen je Agent,
By provider und By key nehmen alle das aufgelöste `since`/`until` statt einer
eigenen Tageszahl, sodass zwei Zahlen nebeneinander nicht verschiedene Runs
beschreiben können. Das ist derselbe Defekt, den #198 ein Panel weiter oben
benennt.

**Seit Monatsbeginn ignoriert das Fenster vollständig**, und ebenso jedes daran
gemessene Cap je Agent. Eine monatliche Obergrenze, verglichen mit rollenden
sieben Tagen, liest sich an dem Tag, an dem das Cap tatsächlich erreicht wurde,
als zu 20 % ausgeschöpft.

Jede Zeile je Agent trägt **zwei Kostenzahlen unter zwei verschiedenen Namen**,
und das ist die Regel dieser Seite durchgehend:

|  |  |
| --- | --- |
| `cost_usd` | Sein Anteil am Fenster, **nur Runs der obersten Ebene**, sodass sich die Spalte auf die Summe darüber addiert |
| `month_to_date_usd` | Sein **eigener** Kalendermonat, delegierte Zeilen **eingeschlossen** — die Ausgaben, auf die sein `monthly_cap_usd` ein Cap ist. Er addiert sich nicht auf den Monat der Organisation und wird auch nicht so gezeichnet |

`partial_run_count` sagt, wie viel davon überhaupt eine Tatsache ist: wie viele
**Runs der obersten Ebene** im Fenster sich nicht vollständig bepreisen ließen,
sodass die Kosten um genau so viele eine Untergrenze sind. _„3 von 40 Runs ließen
sich nicht bepreisen"_ ist etwas, womit ein Leser etwas anfangen kann; eine Zahl
mit einem Pluszeichen daran nicht.

Ein Run zählt, wenn ein Modell in seinem Baum keinen Preis hatte, **die seiner
Delegates eingeschlossen** — der Baum teilt sich ein Spend-Ledger, ein
unbepreister Delegate macht also auch die Zeile des Parents zu einer Untergrenze.
So herrscht eine Zahl über alle drei Aufschlüsselungen: By provider und By key
summieren die eigenen Ausgaben jeder Zeile, delegierte Zeilen eingeschlossen, und
eine Untergrenze in einer von beiden markiert eine Zahl, die die verursachende
Zeile nie angesehen hat. Sie **misst** By agent,
das dieselben Zeilen der obersten Ebene zählt, und **markiert** die anderen
beiden nur — sie zählt Bäume, sodass ein Parent mit drei unbepreisten Delegates
als `1` erscheint, während drei Zahlen darunter eine Untergrenze sind.

Ein Baum, der **über den Anfang des Fensters hinausragt** — die Zeile des
Delegates darin, die seines Parents davor —, wird über den Delegate gezählt: Die
Parent-Zeile, die sonst die Markierung trüge, liegt außerhalb jedes Aggregats
dieser Seite, während die eigenen Ausgaben des Delegates in beiden Aufteilungen
liegen. Sie landet bei dem Agent, als der der Delegate lief, einmal je
hinausragendem Baum, wie viele Delegationen auch die Kante überquerten, und nur
dann, wenn die eigenen Anfragen des Delegates unbepreist blieben — eine bepreiste
Delegation unter einem unbepreisten Parent außerhalb des Fensters wirft keinen
Vorbehalt auf, denn das Geld im Fenster selbst ist exakt
([#620](https://github.com/vstorm-co/agenticos/issues/620)).

Eine Zeile ist **eine je Agent**, mit `agent_name` darauf. Früher war sie eine je
Agent _und Modell_ und trug nur `model_label` — der Tab listete also Modellnamen,
wo ein Leser einen Agent erwartet, und teilte einen Agent auf zwei Zeilen auf,
weil er auf zwei Modellen geantwortet hatte. Die Form je Modell überlebt dort, wo
sie die gestellte Frage ist: Die Nutzungs-E-Mail gruppiert weiterhin so.

**Wer es ausgegeben hat, ist eine vierte Aufschlüsselung**, unterhalb von By
provider, By key und By agent — diejenige, die mit Personen antwortet statt mit
Anbietern oder Agents.

Sie liest dieselben `group_by=user`-Zeilen wie die Adoption-Tabelle des
Dashboards — nur Runs der obersten Ebene, die geschäftigsten zuerst —, sodass die
Kosten eines Delegates einmal landen, innerhalb des Runs, der ihn gestartet hat.
Und sie deckt das Fenster ab, das der Rest des Tabs zeigt, statt einer eigenen
rollenden Voreinstellung.

**Wann es ausgegeben wurde** ist ein Balken pro UTC-Tag über dasselbe Fenster, aus
`by_day` — die eigenen Kosten jedes Runs, sodass die Tage dieselbe Rechnung ergeben,
die die Aufschlüsselungen teilen. Ein Tag ohne Runs wird als null gezeichnet statt
ausgelassen. Jede Aufschlüsselung zeigt den Anteil jeder Zeile an ihrer Summe, und ein
Agent mit Monatslimit zeigt den bisherigen Monat gegen dieses Limit.

Die Personen der Organisation zu benennen ist dieselbe Entscheidung, die auch die
Dashboard-Karte trifft, also nimmt sie dasselbe Gate: `runs:view`, gehalten von
Builder und Operator ebenso wie von den beiden Verwaltern, und sie sagt das in
ihrem eigenen Text.

Ein Aufrufer ohne `runs:view` sieht sie nicht. Die Karte ist abwesend und ihre
Frage wird nie gestellt, statt dass eine Anfrage abgelehnt zurückkommt.

### Die Freigabe-Queue einengen { #narrowing-the-approvals-queue }

`GET /approvals` liefert zwei Sichten auf dieselben Zeilen. Voreingestellt nur
die ausstehenden, das ist die Queue, auf der jemand handelt;
`?status=approved&status=rejected` ist der Datensatz dessen, was entschieden
wurde, und er trägt den Namen und die Notiz der entscheidenden Person, denn eine
nackte UUID ist keine Spur der Verantwortlichkeit. Auf einer entschiedenen Zeile
gibt es bewusst keine Bedienelemente.

| Parameter |  |
| --- | --- |
| `status` | Wiederholbar. Abwesend heißt ausstehend — die Queue |
| `triggered_by_user_id` | Wessen Runs den Aufruf geparkt haben. Abgelesen an `agent_runs`: Eine Freigabe gehört zu einem Run und ein Run zu einer Person. Sie lässt Runs aus, in denen der Veröffentlicher für ein nicht verknüpftes Chat-Konto eingesprungen ist; solche Zeilen tragen stattdessen `asked_in_chat` (Plattform, Name und Bot), und ihre `triggered_by_*`-Felder sind leer |
| `created_from`, `created_to` | Wann der Aufruf geparkt wurde, an beiden Enden einschließlich |
| `oldest_first` | Voreingestellt wahr, und die Voreinstellung ist tragend — siehe oben: Nichts lässt einen Aufruf altern, also würde „neueste zuerst" die Zeile begraben, die am dringendsten gesehen werden muss |

Jede Zeile nennt drei Dinge, die in anderen Tabellen liegen — den Agent, die
Person, deren Run den Aufruf geparkt hat, und die Person, die entschieden hat.
Der Agent und der Run sind Inner Joins, weil beide Fremdschlüssel kaskadieren,
sodass eine Freigabe keinen von beiden überleben kann; die zwei Personen sind
Outer Joins, denn eine Entscheidung muss die Löschung des Kontos ihrer
entscheidenden Person überleben, und der Besucher eines Widgets ist von vornherein
anonym.

### Die Run-Historie einengen { #narrowing-run-history }

| Parameter |  |
| --- | --- |
| `status` | Wiederholbar. `?status=failed&status=budget_exceeded` ist die Zeig-mir-die-Probleme-Abfrage, und die beiden sind genau deshalb getrennte Status, damit nach dem einen zu fragen nicht heißt, nach dem anderen zu fragen |
| `surface` | Woher der Run kam |
| `user_id` | Als wer der Run lief, was nicht immer der ist, der gefragt hat — die Runs eines Widgets tragen die Identität des Widget-Besitzers, weil der Besucher anonym ist |
| `model_label` | Das Modell **so, wie der Run es festgehalten hat**, exakt verglichen. Nicht über den Modellkatalog aufgelöst: Die Spalte ist das, was geantwortet hat, und ein Profil, aus dem es kam, kann seither umbenannt oder gelöscht worden sein. Die Modellkarte des Dashboards zählt dieselben Zeichenketten, sodass „die Runs hinter diesem Balken" auf beiden Bildschirmen eine Menge ist |
| `started_from`, `started_to` | An beiden Enden einschließlich, denn ein Bereichswähler übergibt ganze Tage |
| `environment_id` | Runs auf der Version, die dieses Environment festpinnt. **Nie ein delegierter Run:** Die Version eines Delegates kommt von einem Pin, also wird die Spalte auf einem solchen bewusst nie geschrieben, und auf `production` einzuengen lässt jede Delegation fallen. Eine Oberfläche, die Delegationen einschließt, muss das sagen |
| `exposure_id` | Runs, die über eine Bindung zugelassen wurden. Null für das Dashboard und die API |
| `agent_version_id` | Runs, die einen eingefrorenen Spec ausgeführt haben — das „zeig mir die Zeilen hinter dieser Zahl" der Versionsleiste |
| `rated` | `down` oder `up` — Runs, bei denen jemand eine vom Run erzeugte Message bewertet hat |
| `order_by`, `descending` | `started_at` (die Voreinstellung, neueste zuerst), `duration`, `cost` oder `tokens` |

**Jeder Filter engt die Zählung ebenso ein wie die Seite**, sodass `total` immer
die Zeilen darunter beschreibt. Die Liste und die Zählung sind zwei Abfragen, und
eine Bedingung, die nur eine von beiden erreicht, liest sich als Paging-Fehler
statt als fehlende Klausel.

`started_from` ist auch das, was diese Zählung mit dem Geld daneben in Einklang
bringen lässt. Ungefenstert liest sie _alle Zeit_, während eine Ausgabenzahl
einen Kalendermonat liest, sodass eine drei Jahre alte Organisation „8.412 Runs"
neben „31,20 $" zeigte und die naheliegende Lesart des Paars um drei Jahre falsch
war. Eine Zahl und eine Ausgabenzahl auf einem Bildschirm teilen sich ein Fenster,
oder sie sagen, welches Fenster jede meint.

Ein Wert außerhalb seines Typs wird mit einem 422 abgelehnt, statt gegen nichts
abgeglichen zu werden: `status` und `surface` sind Zeichenkettenspalten, sodass
`?status=complete` sonst mit einer leeren Seite antwortete — und eine leere Seite
liest sich als _diese Woche ist nichts schiefgegangen_. `order_by` nimmt eine von
vier Ordnungen statt eines Spaltennamens, aus demselben Grund plus einem
weiteren: Ein aus einem Query-String zusammengesetztes `ORDER BY` ist eine
Angriffsfläche für Injection.

**Jeder Einzelne davon reist in der URL**, und das ist es, was eine
Dashboard-Karte an ihre eigenen Zeilen übergeben lässt:
`/runs?surface=mattermost&period=30d` öffnet **Aktivität** (Aktivität) mit der bereits gesetzten
Facette und einer Zählung, die zu der Karte passt, die verlinkt hat. Bis #768
waren sie lokaler State, sodass die p95-Zahl die einzige Zahl auf dem Dashboard
war, die die Runs dahinter erreichen konnte, und drei Karten überhaupt keinen
Link trugen — es gab nichts Ehrliches, worauf man sie hätte zeigen lassen können.

**Einschließlich dessen, welcher Tab offen ist.** `?tab=approvals` und
`?tab=spend` öffnen die Queue und den Kostenbildschirm; die Run-Historie ist die
Voreinstellung und wird nie geschrieben.

Das ist die Adresse, die ein Link auf eine _Entscheidung_ braucht, und der Grund,
warum es den Parameter gibt: Das „See all" der Freigabe-Karte und der Alert, der
sagt, dass ein Run geparkt ist, mussten beide auf die Run-Historie zeigen, wo
sich nichts entscheiden lässt (#934).

Ein von einem Link genannter Tab wird **gegen das aufgelöst, was der Leser öffnen
darf**. `approvals` ist auf `approvals:decide` gegated, sodass ein Link, der ihn
trägt und jemanden ohne die Berechtigung erreicht, die Run-Historie öffnet statt
einer Leiste, deren ausgewählter Tab keinen Inhalt hat.

Den Tab zu wechseln schließt ein offenes Run-Detail und nimmt `?run=` mit. Ein
Panel, das den Tab überlebt, der es geöffnet hat, steht neben einer Queue, mit
der es nichts zu tun hat — und unterhalb von `lg` ersetzt es die Liste, sodass die
Leiste lebendig blieb, während der Inhalt jedes Tabs verborgen war.

**Die Dauer wird in SQL berechnet, über die ganze eingeengte Menge.**

Das ist es, was von _„p95 ist 14,8 s"_ auf dem Dashboard zu **diesen Runs**
führt. Eine Seite mit fünfundzwanzig Zeilen zu sortieren sortiert die falsche
Menge, denn der langsamste Run eines Monats steht nicht in den Zeilen, die eine
Seite mit den neuesten zuerst zufällig zurückgegeben hat. `cost` und `tokens`
sind dieselbe Anordnung für Geld und Kontextgewicht.

Ein Run ohne `ended_at` sortiert unter allen dreien **in beiden Richtungen als
Letztes**. Er hat keine Dauer, er ist auch nicht der schnellste Run, und seine
Kosten- und Token-Zahlen werden erst geschrieben, wenn er fertig ist — so
sortiert, wie gespeichert, läse sich ein noch laufender Run als der billigste und
leichteste in der Organisation.

Wie lange ein _noch laufender_ Run schon läuft, ist eine andere Frage, und keine
dieser Ordnungen beantwortet sie.

**Aktivität** (Aktivität) zeigt diese Dauer auf drei Wegen, und alle drei führen zu derselben
Abfrage:

- Die Spaltenüberschrift **Took** ist ein Sortier-Bedienelement — wie die
  Überschrift Started daneben und wie jede sortierbare Überschrift im Produkt —,
  sodass ein Klick die Historie nach `duration` neu ordnet und nicht die
  fünfundzwanzig Zeilen auf dem Bildschirm.
- Die **p95-Zahl des Dashboards verlinkt hierher**, nach Dauer sortiert über
  dasselbe Fenster: `?sort=duration` mit dem `started_from` / `started_to` des
  Zeitraums.

So sind die Zahl und die Runs dahinter einen Klick voneinander entfernt — die
Regel, der der Rest dieser beiden Seiten schon folgt, und die eine Dimension, in
der sie es nicht taten (#210).

**`rated=down` ist hier die Queue mit dem höchsten Signal** — die Antworten, von
denen echte Menschen sagten, sie seien falsch, in ihren eigenen Worten. Eine
Bewertung hängt an einer Message, also läuft dieser Join über `messages.run_id`:
Zwei Runs in einer Unterhaltung behalten ihre eigenen Bewertungen, und deshalb
gibt es diese Spalte statt eines Zeitfensters über den Faden. Es ist ein
`EXISTS`, sodass ein Run, den drei Personen schlecht fanden, eine Zeile ist und
nicht drei; und ein Run, den eine Person mochte, während eine andere ihn schlecht
fand, passt auf **beides**, `up` und `down`, weil beides von ihm wahr ist. Das auf
ein Urteil je Run zu reduzieren hieße, einen Konsens zu erfinden, den die Zeilen
nicht festhalten.

Dieselbe Tatsache reist auch ohne den Filter auf der Zeile mit.
`AgentRunRead.down_rated` ist `true`, wenn irgendwer eine vom Run erzeugte
Antwort unter null bewertet hat, für eine Seite in einer Abfrage berechnet, und
es ist das, worauf die Run-Historie ein 👎 zeichnet. Es ist wie jeder Lesevorgang
hier auf die Organisation des Aufrufers begrenzt — der negativ bewertete Run
eines Nachbarn wird nie für einen anderen Tenant markiert.

Der **Kommentar**, mit dem dieser Daumen hinterlassen wurde, wird im Run-Detail
gelesen (`?run=<id>`), nicht auf der Zeile. Es ist von Nutzern geschriebener Text
über eine Unterhaltung, und ihn hinter das Detail zu legen ist die bewusste Linie
zwischen einer Markierung, die jeder mit `runs:view` sieht, und den Worten, die
sie erklären.

Das ist der Join, für den `rated=down` gebaut wurde: Das Dashboard sagt, die
Qualität sei um vier Punkte gefallen, und hier werden die Unterhaltungen gelesen,
die das getan haben.

Den Trend liest das Dashboard über `GET /api/v1/ratings/summary` (eine
Schlagzahl-Aufteilung plus eine Reihe je Tag): `scope=org` unter `runs:view`,
`scope=own` für die eigenen Unterhaltungen eines Mitglieds, dieselbe
Scope-Regel und dasselbe Fenster-Vokabular wie `GET /stats/usage` (siehe
[Berechtigungen](permissions.md)). Nur Zählungen — die Kommentare bleiben hinter
dem Run-Detail oben.

Die drei Zahlen in **Aktivität** (Aktivität) über den Tabs bleiben die der Organisation,
einschließlich der Run-Zählung, auch wenn die Tabelle darunter auf einen Agent
eingeengt ist. Eine Zählung je Agent neben dem Monat der Organisation wären zwei
Fragen unter einem Etikett — und die Zählung je Agent ist diejenige, die
Delegationen einschließt.

**Eine verwaiste Delegation wird ohne ihr Handle gemeldet.** `parent_run_id` ist
`ON DELETE SET NULL`, sodass das Löschen des Parents eine Zeile hinterlässt, die
korrekt beginnt, zur Rechnung zu zählen — aber ein Fremdschlüssel kann nur seine
eigene Spalte auf null setzen, und die gespeicherte `subagent_task_id` benennt
dann ein Transkript, das mit dem Parent gegangen ist. `AgentRunRead` hält sie
zurück, wann immer `parent_run_id` null ist, sodass keine Oberfläche ein
Delegations-Handle anbietet, das nichts erreicht.

### Nach CSV exportieren { #exporting-to-csv }

Alles, was die drei Tabs zeigen, lässt sich als CSV vom Bildschirm nehmen: die
Zeilen, die jemand gegen eine Rechnung abgleicht, einem Finanzteam übergibt oder
an ein Audit hängt. Eine Seite, die die Frage auf dem Bildschirm beantworten kann
und außerhalb davon nicht, schickt Menschen in die Datenbank.

| Frage | Antwort |
| --- | --- |
| `GET /runs/export` | Die Run-Historie, dieselben Filter wie `GET /runs` und dieselbe Voreinstellung nur oberste Ebene. `runs:view` |
| `GET /approvals/export` | Der Freigabe-Datensatz, dieselben Filter wie `GET /approvals`. `approvals:decide` |
| `GET /spend/export` | Die Ausgaben-Aufschlüsselung je Agent, dasselbe Fenster wie `GET /spend`. `runs:view` |
| `GET /audit/export` | Die Audit-Spur über ein Fenster, CSV oder JSONL (`?fmt=`). `audit:read` |

Der Audit-Export ist der einzige, der auch **JSONL** anbietet (`?fmt=jsonl`),
ein JSON-Objekt pro Zeile, weil eine Audit-Spur ebenso oft von einer
Log-Pipeline eingelesen wie in einer Tabelle geöffnet wird; beide beschreiben
dieselben Einträge, mit `details` als JSON-String in der CSV-Zelle und als
verschachteltes Objekt in den Zeilen. Er liefert genau die Felder, die das
Lesemodell von `GET /audit` zeigt — die gespeicherte `ip_address` steht nicht auf
diesem Tab und daher auch nicht im Export — und hält wie jeder Export hier seinen
eigenen Abruf in der Spur fest (`audit.export`, mit Fenster, Format und
Zeilenzahl).

Der Ausgaben-Export trägt nur die Zahlen des Fensters — `cost_usd`, `run_count`
und `partial_run_count`. `month_to_date_usd` und `monthly_cap_usd` des Spend-Tabs
bleiben weg: Sie lesen den Kalendermonat, während `cost_usd` das Fenster des
Exports liest, und zwei Dollarspalten auf zwei Zeitbasen in einer
heruntergeladenen Datei werden von einem Leser, der den Unterschied nicht sieht,
quer aufsummiert. Eine heruntergeladene Datei trägt eine Zeitbasis, nämlich das
Fenster, nach dem gefragt wurde.

Ein Export ist ein Massen-Lesevorgang und keine Schaltfläche, und er beantwortet
sechs Fragen, die die Listen-Routen nicht beantworten müssen:

- **Mandantentrennung.** Jeder Export trägt das Gate des Tabs, aus dem er kommt,
  und jeder Lesevorgang ist auf die Organisation des Aufrufers begrenzt — die
  Zeilen eines Nachbarn erreichen ihn nie, einschließlich einer Zeile, die dem
  Aufrufer in einer Organisation gehört, die nicht die ist, aus der er fragt. Die
  zwei auf `runs:view` legen in der Abfrage zusätzlich einen **Boden von
  `Scope.OWN`** ein: Ein Aufrufer, dessen `runs:view` weniger als die ganze
  Organisation erreicht, exportiert nur seine eigenen Zeilen,
  `WHERE user_id = <them>`, und eine `user_id`, die er übergibt, wird mit seiner
  eigenen überschrieben, statt sie zu weiten. Keine eingebaute Rolle hält
  `runs:view` bisher unterhalb von `all`; der Boden liegt bereit für den
  Zeitpunkt, an dem die Scope-Entscheidung für Member und Viewer fällt.
- **Größe.** Ein Export hat von Natur aus keine Obergrenze, also bekommt er eine
  per Design. Der **Datumsbereich ist verpflichtend** — eine Anfrage ohne beide
  Enden wird abgelehnt — und die Treffermenge ist **auf 10.000 Zeilen
  gedeckelt**, darüber wird die Anfrage mit einer Nachricht abgelehnt, die die
  Anzahl nennt und dem Aufrufer sagt, er solle den Bereich einengen. Niemals eine
  stille Kürzung: Eine beschnittene CSV ist schlimmer als eine abgelehnte, denn
  eine Tabellenkalkulation summiert, was ankommt. Das Cap ist es, was den Body in
  einem Durchgang bauen und den Audit-Eintrag committen lässt, bevor die Antwort
  hinausgeht, statt ihn über eine gehaltene Verbindung zu streamen.
- **Teilweise Kosten.** `cost_is_partial` ist im Run-Export eine eigene Spalte und
  `partial_run_count` im Ausgaben-Export, sodass eine Untergrenze eine
  Tabellensumme überlebt. Ein Run, dessen einziges Modell unbepreist war,
  exportiert seine echten `cost_usd` von `0` neben `cost_is_partial=true` — nie
  eine nackte `0`, die ein Leser für kostenlos hält.
- **Delegierte Runs.** Der Run-Export nimmt voreingestellt nur Zeilen der obersten
  Ebene, genau wie die Liste, sodass die Summe von `cost_usd` die Rechnung ergibt
  und nicht das Doppelte. Die Haltung steht in der Datei und nicht nur hier: Jede
  Zeile trägt eine Spalte `parent_run_id`, leer für einen Run, den jemand
  gestartet hat, und gesetzt für eine Delegation, sodass ein Leser, der sich für
  `include_delegations` entscheidet, sehen kann, welche Zeilen bei einer
  Gesamtsumme doppelt zählen würden.
- **Personenbezogene Daten.** Jeder Export liefert genau die Identität, die sein
  Tab ohnehin zeigt. Die Run-Tabelle zeigt eine `user_id` und keinen Namen, also
  liefert der Run-Export allein die Id — eine CSV, wer was ausgeführt hat, mit
  aufgelösten Namen, ist die Tabelle je Person, die Entscheidung 3 des
  Designs für **Aktivität** (Aktivität) abgelehnt hat, angekommen als Download. Die Freigabe-Queue löst
  die auslösenden und entscheidenden E-Mail-Adressen auf dem Bildschirm bereits
  auf, also behält der Freigabe-Export sie. Er enthält auch den Anzeigenamen oder das Handle eines Chat-Fremden, wie die Queue sie zeigt, in drei Spalten: `asked_in_chat_platform`, `asked_in_chat_account` und `asked_in_chat_bot`. In einer solchen Zeile sind beide E-Mail-Spalten leer, weil der Veröffentlicher nicht gefragt hat.
- **Audit.** Jeder Export schreibt einen `audit_log`-Eintrag — ein privilegierter
  Massen-Lesevorgang, jetzt billig festzuhalten und später unmöglich zu
  rekonstruieren. Er nennt das Fenster, die angewendeten Filter und die
  Zeilenzahl, nie den Anfrage-Body.

### Ein gepinnter Delegate bewegt sich nicht von allein { #a-pinned-delegate-does-not-move-on-its-own }

Ein Delegate ist auf eine Version gepinnt, sodass ein Fix, den sein Autor
ausliefert, für seine Aufrufer nichts ändert, bis jemand den Parent gegen den
neuen Pin neu veröffentlicht. Das ist dieselbe Garantie, die das Veröffentlichen
hier überall gibt, und sie schneidet in beide Richtungen: Ein in einem Delegate
behobener Fehler ist ein Fehler, der in jedem Parent, der sich nicht bewegt hat,
weiterlebt.

Der Builder ist der Ort, an dem das sichtbar gemacht wird — er vergleicht jeden
Pin mit dem, was der Delegate jetzt veröffentlicht, und bietet an, ihn zu
verschieben —, denn Veralterung, die nichts sichtbar macht, ist ein festgefrorener
Fehler. Ein Pin, dessen Version es nicht mehr gibt, **lässt den Run scheitern**
und nennt den Delegate; nie ein leises Zurückfallen auf die aktuelle Version.

**Einen Delegate zu archivieren hört auf, ihn antworten zu lassen, auch als
jemandes Delegate.** Ein Pin auf einen bereits archivierten Agent wird beim
Veröffentlichen abgelehnt, und ein Agent, der nach dem Pinnen archiviert wurde,
lässt den Run seines Aufrufers namentlich scheitern — sonst würde einen Agent
außer Dienst zu nehmen ihn ausgerechnet an der einen Stelle, an der niemand
hinsieht, unbegrenzt weiterlaufen lassen, und der Autor, der ihn ausgemustert
hat, erführe es nie.

### Step-Limits { #step-limits }

Die andere Art von Ausreißer ist eine Tool-Schleife: billig je Aufruf, und sie
wird nie fertig. Ein Budget rechnet dafür nur ab. `max_steps` deckelt, wie viele
Modellanfragen ein Run stellen darf, und ist das, was sie tatsächlich stoppt.

Der Schleifenwächter stoppt die meisten Schleifen früher und sagt, warum. Jeder
Agent hat ihn, standardmäßig eingeschaltet. Ruft ein Run ein Tool viermal in
Folge mit denselben Argumenten auf oder durchläuft er einen Zyklus aus zwei oder
drei Aufrufen dreimal, wird dem Modell gesagt, dass es in einer Schleife steckt
und anders vorgehen soll. Laufen die Aufrufe nach dieser Warnung wieder in eine
Schleife, endet der Run als `loop_stopped`, mit einer Begründung, die die Tools
nennt - nie ihre Argumente. Aufrufe mit anderen Argumenten sind nie eine
Schleife, und nur die Aufrufe des Runs selbst zählen. Der Builder setzt beide
Schwellen oder schaltet den Wächter aus, neben `max_steps` (`loop_guard` im
Spec).

### Run-Reviewer { #run-reviewers }

Ein langer Run driftet: Anweisungen verblassen, Behauptungen ohne Quelle häufen
sich, der Agent entfernt sich von der Anfrage. Ein Agent kann bis zu fünf
Reviewer haben (`reviewers` im Spec, _Run-Reviewer_ im Builder neben den
Limits), jeder ein zweites, meist günstigeres Modell mit einem Anliegen. Alle
`every` Modell-Requests liest ein Reviewer die letzten `window` Tokens des Runs
und lässt ihn entweder laufen oder schickt eine kurze Korrektur, die das Modell
des Runs beim nächsten Request mit dem Namen des Reviewers erhält. Die
Run-Ansicht listet jede Korrektur und wer sie geschickt hat.

Die Aufrufe eines Reviewers werden vor dem Absenden gegen das Budget des Agents
und der Organisation geprüft und auf dem Run gebucht, bepreist als das Modell des
Reviewers. Ein wegen des Budgets abgelehnter Aufruf wird übersprungen, und ein
Reviewer, der scheitert, wird geloggt; keines von beiden lässt den Run scheitern.
Ein Agent ohne Reviewer macht keine zusätzlichen Modellaufrufe.

### Code-Modus { #code-mode }

Ein Agent, der Datensätze abarbeitet - zehn Zeilen aktualisieren, dann
zusammenfassen -, zahlt für jeden abhängigen Aufruf einen Modell-Turn, und jeder
Turn schickt den ganzen Kontext erneut. Mit eingeschaltetem _Code-Modus_
(`code_mode` im Spec, neben den Limits im Builder) werden die Tools des Agents in
ein einziges `run_code`-Tool gefaltet: Das Modell schreibt ein kurzes
Python-Skript, das in der Monty-Sandbox läuft und die Tools in Schleifen und
parallel aufruft, bis zu `max_tool_calls` pro Skript.

Höchstens vier Aufrufe eines Skripts laufen gleichzeitig, der Rest wartet, sodass
ein Skript, das Hunderte Sandbox-Aufrufe auffächert, sie nicht alle zugleich
öffnet.

Jeder Aufruf aus einem Skript durchläuft dieselben Guardrails, dasselbe Budget
und dieselben Berechtigungsprüfungen wie ein direkter Aufruf. Ein Tool, das eine
Freigabe braucht, wird nie hineingefaltet: Es bleibt ein normaler Aufruf, und der
Run hält wie immer für die Person an. Ebenso Rückfragen, Delegationen und
Tools, deren Aufruf das ist, was die Person sieht (Entwürfe, Diagramme,
Zeichnungen). Die Run-Ansicht listet jedes Skript und
jeden Aufruf, den es gemacht hat.

### Berichte { #reporting }

Ein Run, der sich nicht bepreisen ließ — ein Modell, das `genai-prices` nicht
kennt —, wird mit einer Warnung bei null festgehalten, und die Summe wird als
**Untergrenze** gekennzeichnet, statt geraten zu werden. Die UI zeigt das als ein
`+` neben der Zahl.

**Cache-Lese- und Schreibvorgänge werden getrennt geführt.** Die Eingabetokens
eines Runs enthalten die, die der Anbieter aus seinem Prompt-Cache bedient hat,
und die, die er hineingeschrieben hat, und alle drei werden getrennt bepreist -
bei Claude Sonnet 5.5 kostet ein Cache-Lesevorgang 5 % des Eingabepreises und ein
Schreibvorgang 125 %. Jeder Run und jede Antwort speichert `cache_read_tokens` und
`cache_write_tokens` neben der Summe, die Kosten werden aus dieser Aufteilung
berechnet, und die Run-Ansicht und die Kosten unter jeder Antwort zeigen sie -
damit lässt sich ein Run mit der Konsole des Anbieters abgleichen.

**Die Kosten eines Runs enthalten seine Delegationen.** Der Run, den jemand
gestartet hat, trägt das ganze Ledger, Delegates eingeschlossen; die eigene Zeile
einer Delegation teilt dasselbe Geld nach Agent auf, und die Run-Ansicht sagt,
dass ihre Delegationen schon in der Zahl darüber stecken. Beide zu addieren zählt
die Delegationen doppelt.

**Ein Prompt-Cache, der nicht mehr trifft, wird markiert.** Caching lohnt sich
nur, solange der Anfang jedes Requests gleich bleibt; eine umgestellte Tool-Liste
oder ein Zeitstempel in den Anweisungen lässt den Anbieter den vollen Preis für
Tokens berechnen, die er im Cache hatte. Jeder Agent liest bei jeder Antwort die
Cache-Zahlen des Anbieters, und ein Request, dessen gecachter Präfix
eingebrochen ist, wird im Request-Wasserfall des Runs markiert - _Cache
gebrochen_, solange der Anbieter ihn noch hätte halten sollen, _Cache
abgelaufen_, wenn die Aufbewahrung vorbei war - mit der Zahl der gecachten
Tokens, die er nicht zurückgelesen hat, und mit Agent und Modell geloggt. Der erste Request eines
Turns wird mit dem verglichen, was der vorige Turn gecacht hat, wenn derselbe
API-Worker beide bedient.

Die Zahlen werden an jedem Run gespeichert, daher summiert **Runs → Spend**, wo
der Cache nicht gehalten hat: eine Karte _Prompt-Cache_, eine Zeile pro Agent
und Modell, mit Busts, Abläufen und den gecachten Tokens, die nicht zurückgelesen
wurden, im Zeitraum - ein Maß für den Fehlgriff, nicht für erneut Berechnetes, da
auch eine Komprimierung den gecachten Prompt verkleinern kann. Solange jeder Cache gehalten hat, erscheint die Karte nicht.

## Freigaben { #approvals }

Ein Tool, das der Autor des Agents hinter ein Gate gestellt hat, parkt den Run und
wartet auf eine Person. Nichts steht hinter einem Gate, bis jemand darum bittet:
Ein Gate, das niemand gewählt hat, parkt Runs in Kanälen und Routinen, in denen
niemand da ist, um zu antworten.

Die Auflösung geht vom Spezifischsten aus:

1. die eigene Übersteuerung des Tools, falls es eine hat
2. der `approval`-Modus der Capability (`required` | `never` | `default`)
3. keine Freigabe, wenn beide `default` sagen

`side_effecting` entscheidet hier nichts mehr. Der Builder zeigt es neben der
Wahl, sodass ein Autor die Tools, die auf die Welt einwirken, zuerst sieht.

Der Builder benennt das Ergebnis in Worten, statt die Regel zu beschreiben, denn
eine Regel, die der Leser im Kopf ausführen muss, ist eine Einstellung, an die
sich niemand herantraut.

```mermaid
sequenceDiagram
    autonumber
    participant M as Model
    participant G as ApprovalGate
    participant Q as Approvals queue
    participant P as A person
    M->>G: call a gated tool
    G->>Q: park it, with the arguments
    G-->>M: run ends `awaiting_approval`
    P->>Q: reads the arguments, decides
    alt approved
        Q->>G: resume with the arguments that were read
        G->>M: execute those, not what it proposes now
    else rejected
        Q->>G: resume, replaying the denial
        G->>M: a refusal it can relay, not a crash
    else expired
        Q--xM: the run ends `cancelled`. No further model request
    end
```

Fünf Eigenschaften, die man kennen sollte:

- **Ein geparkter Run ist fortsetzbar.** Seine Message-Historie ist gespeichert,
  sodass die Entscheidung auf die Unterhaltung angewendet wird, zu der sie
  gehört, statt neu zu beginnen.
- **Ein geparkter Run überlebt ein Neuladen und sagt das.** Das Transkript
  speichert den Aufruf, auf dem der Run stehen blieb, als `awaiting_approval`
  statt als `running`, sodass das erneute Öffnen der Unterhaltung den wartenden
  Schritt weiterhin zeigt — und `GET /runs/{id}/parked` antwortet mit den
  ausstehenden Aufrufen (die zu entscheidende Freigabe, das Tool, seine
  Argumente), und so baut der Chat das Freigabe-Panel wieder auf, das der
  Live-Frame `tool_approval_required` demjenigen gab, der gerade zusah. Er
  antwortet für einen Run, der nicht geparkt ist, leer, und er ist wie die Queue
  auf `approvals:decide` gegated, weil seine Zeilen zur Entscheidung angeboten
  werden ([#601](https://github.com/vstorm-co/agenticos/issues/601)). Der Schritt
  liest sich nicht als ewig wartend: Ein Fortsetzen erledigt ihn mit dem, was der
  Aufruf zurückgab, ein Ablauf mit dem Hinweis auf das Zeitlimit.
- **Eine Fortsetzung sagt, was sie getan hat.** `POST /runs/{id}/resume` antwortet
  mit den Tool-Aufrufen, die die Fortsetzung gemacht hat, der Reihe nach, jeder
  mit dem, was zurückkam — und das Transkript hält sie fest, ob sie zu einer
  Antwort gelangte oder nicht. Beide Hälften fehlten früher, und eine Freigabe
  konnte eine unbegrenzte Menge Arbeit verbergen: Der Agent lief innerhalb der
  Resume-Anfrage statt auf dem Socket, den die Unterhaltung streamt, also kündigte
  nichts seine Aufrufe an, und der Transkript-Schreibvorgang wurde für ein Segment
  ohne Antwort übersprungen. Ein Run, der eine Datei las und dann darum bat, einen
  zweiten Befehl auszuführen, zeigte zwischen den beiden Freigaben nichts und hielt
  auch nichts fest.
- **Und was der freigegebene Aufruf selbst zurückgab, landet auf dem Schritt, der
  freigegeben wurde.** Es kommt getrennt von den eigenen Aufrufen der Fortsetzung
  an (`settled`, nicht `steps`), weil es von der Ausführung gemacht wurde, die
  geparkt hat: Das Fortsetzen erzeugt seine Rückgabe ohne den Aufruf, zu dem sie
  gehört, also schließt es eine bereits geschriebene Zeile, statt eine neue zu
  öffnen. Es als Schritt festzuhalten würde denselben Befehl zweimal in den Turn
  setzen; es überhaupt nicht festzuhalten — was geschah, bis es geschah — machte
  den einen Aufruf, den jemand bewusst geprüft hat, zu dem einen Aufruf ohne
  Ausgabe irgendwo.
- **Er bleibt fortsetzbar, wenn das Fortsetzen scheitert.** Ein Run wird auf der
  Version fortgesetzt, auf der er geparkt hat, und der Spec dieser Version kann
  seither aufgehört haben zu bauen — ein von einer Bindung benanntes Secret
  gelöscht, ein Modellprofil entfernt, eine Capability in einem Deploy
  weggefallen, eine MCP-Verbindung nicht mehr geteilt. Der Spec wird
  zusammengesetzt, bevor der Run die Freigabe-Queue verlässt, sodass eine
  Ablehnung dort den _Versuch_ ablehnt: Die Entscheidung steht, und das Fortsetzen
  funktioniert wieder, sobald der Spec es tut. Ein im Chat gestellter Run, den
  niemand fortgesetzt hat, wird vom Sicherheitsnetz etwa eine Stunde nach seiner
  letzten Entscheidung beendet.
- **Wer einen im Chat gestellten Run fortsetzt.** Die Entscheidung einer Person in
  der Konsole, im Web-Chat oder über die API setzt den Run in seinem Chat fort,
  als sie, und die Antwort wird dort gepostet; die Entscheidung eines Schlüssels
  wird festgehalten und setzt nichts fort. Eine Fortsetzung kann verloren gehen
  oder abgelehnt werden, dann wartet der Run in `awaiting_approval` ohne etwas
  Offenes. Das stündliche Sicherheitsnetz beendet einen solchen Kanal-Run
  `STRANDED_AFTER` (eine Stunde) nach seiner letzten Entscheidung und sagt es dem
  Chat einmal - nicht, wenn die Entscheidung mehr als 7 Tage alt ist, und nicht,
  wenn der Chat es schon erfahren hat. Es betrachtet nur Kanal-Runs. Siehe
  [Außerhalb des Chats entschieden](channels.md#decided-outside-the-chat).
- **Eine entschiedene Freigabe kann nicht zweimal entschieden werden.** Die zweite
  Entscheidung wird abgelehnt — einschließlich einer Entscheidung, die eine
  Sekunde nach dem Ablauf-Sweep ankommt, der den Aufruf genommen hat.
- **Ein geparkter Aufruf wird durch Zeitablauf verweigert, sobald er
  `APPROVAL_EXPIRY_HOURS` überschreitet**, und der Run dahinter wird beendet statt
  für immer geparkt zu bleiben. Der Status ist `expired` mit einem
  `decided_by_user_id` von null, und das ist es, was in der Verantwortungsspur
  einen Ablauf von einer Zurückweisung unterscheidet. Ein im Chat gestellter Run
  sagt außerdem dem Chat einmal, dass die Anfrage abgelaufen ist. Die Aktivität-Seite zeigt
  weiterhin das **Alter** der ältesten Wartezeit, denn eine Queue innerhalb ihres
  Ablauffensters ist die, auf der jemand noch handeln kann.
- **`required` funktioniert auf jeder Capability**, nicht nur auf
  nebenwirkungsbehafteten. „Das liest nur, aber in meiner Organisation gibt das
  trotzdem jemand frei" ist eine echte Entscheidung und lässt sich ausdrücken.
- **Außer auf einem Tool, das der Modell-Provider ausführt, wo es beim
  Veröffentlichen abgelehnt wird.** Das Gate umschließt die _Tool-Ausführung_, und
  das ist der einzige Ort, an dem sich ein Aufruf halten lässt, sodass ein natives
  Fetch oder eine native Suche — auf der Seite des Providers ausgeführt — es nie
  erreicht, und eines davon zu gaten würde die Queue leer lassen, während der Agent
  ohne Freigabe handelt. Welche Konfigurationen welche Tools übergeben, erklärt die
  Capability selbst (`provider_executed` in ihrem `register(...)`), sodass die
  Ablehnung jede Capability abdeckt, die eine vom Provider ausgeführte Methode
  bekommt, statt nur die, die ein Validator zufällig kannte
  ([#857](https://github.com/vstorm-co/agenticos/issues/857)). Wählen Sie eine
  Methode, die dieses Deployment selbst ausführt, oder lassen Sie die
  Freigabepflicht fallen; beides sind legitime Agents, und welches gewollt ist, ist
  keine Entscheidung, die stellvertretend für den Autor getroffen wird.
- **Und eine Version, die veröffentlicht wurde, bevor es diese Ablehnung gab,
  läuft nicht.** Nichts validiert eine eingefrorene Version erneut — ein Run lädt
  seinen gespeicherten Spec und setzt ihn zusammen —, also läuft dieselbe Prüfung
  erneut, wenn der Agent gebaut wird, und lehnt ab, statt die Methode still zu
  tauschen, damit das Gate funktioniert. Der Preis ist real und gewollt: Ein
  Agent, der so gelaufen ist, hört auf, mit einer Nachricht, die sagt, was zu
  ändern ist. Was aufhört, ist ein Agent, dessen Betreiber um eine Freigabe bat,
  um die nie jemand gebeten wurde.
- **Ein geparkter Aufruf kann nennen, wofür seine IDs stehen.** Ein Tool kann
  seinen eigenen Aufruf beschreiben: die Schreibvorgänge in Tabellen nennen den Namen der Tabelle.
  Ein Datensatz wird nie genannt, denn das Label eines Datensatzes ist der Text
  einer seiner Zellen, und die Freigabe bewahrt es auf. Die Namen werden als die
  Person des Runs aufgelöst, wenn der Aufruf parkt, sodass nichts genannt wird,
  was diese Person nicht sehen darf, und an der Freigabe als `context`
  gespeichert. Die Karte im Chat und die Seite Approvals beginnen mit einem daraus
  gebauten Satz, etwa „Share the view Closed tickets of Support with everyone who
  may read the table“, über den Argumenten, die weiterhin das sind, was ausgeführt
  wird. Der Satz wird über die stabile ID des Tools gefunden, sodass ein Tool, das
  ein Binding umbenannt hat, ihn behält. Eine fehlgeschlagene oder langsame
  Beschreibung parkt den Aufruf ohne sie, und ein Aufruf, den ein fortgesetzter
  Run bereits entschieden hat, wird nicht erneut beschrieben.
- **Ein Modellschritt kann mehrere Aufrufe parken.** Ein Modell, das mit zwei
  nebenwirkungsbehafteten Aufrufen zugleich antwortet — „mail den Kunden und den
  Kundenbetreuer an" — parkt beide, jeder mit einer eigenen Freigabe-Zeile, die
  für sich entschieden wird. Die Zeilen werden geschrieben, wenn der Run parkt,
  statt beim Gaten jedes Aufrufs, weil die Aufrufe nebenläufig laufen und die
  Datenbank-Session des Runs nicht nebenläufigkeitssicher ist
  ([#169](https://github.com/vstorm-co/agenticos/issues/169)).

### Wie sehr eine Unterhaltung gefragt werden will { #how-much-one-conversation-wants-to-be-asked }

Die Freigabekarte bietet auch **Dieses Tool in diesem Chat erlauben** und **Dieses Tool für diesen Agenten immer erlauben**. Die Auswahl gilt nur für das benannte Tool und Konto, die zustimmende Person und die veröffentlichte Agentenversion. Sie erfordert dieselbe Berechtigung und Organisationseinstellung wie die Freigabe aller Aktionen. Sie autorisiert weder Delegierte noch unbeaufsichtigte Ausführungen. Eine neue Agentenversion erfordert eine neue Auswahl.

Die Auswahl wird auf dem Server gespeichert. **Chat controls → Preferences → Gespeicherte Tool-Freigaben → Erneut fragen** widerruft eine Zustimmung, ohne die ursprüngliche Entscheidung zu löschen. Vor jeder automatischen Freigabe werden Mitgliedschaft, Freigabeberechtigung und Organisationsrichtlinie erneut geprüft. Der Modus zum Nachfragen bei jeder Aktion hat Vorrang. Jede automatische Freigabe protokolliert weiterhin die zustimmende Person und die tatsächlichen Aufrufargumente.

Die Regel oben gehört dem Agent, wird beim Veröffentlichen und je Tool
entschieden, und das ist der richtige Ort dafür: Sie ist eine Aussage darüber, was
der Agent _ist_. Was sie nicht ausdrücken kann, ist die Stimmung einer Sitzung —
wer zwanzig Turns lang mit einem Agent arbeitet, der drei Tools gatet, beantwortet
in jedem Turn dieselben drei Fragen, und der einzige Ausweg war, den Agent neu zu
veröffentlichen und ihn für alle dauerhaft zu ändern, um einen Nachmittag zu
retten ([#925](https://github.com/vstorm-co/agenticos/issues/925)).

Eine Chat-Sitzung trägt also einen **Freigabe-Modus**, auf dem Send-Frame neben
der Modell-Übersteuerung und in den Run hineingelesen:

| Modus | Was er tut |
| --- | --- |
| **Follow the agent** (Voreinstellung) | Der Spec entscheidet. Genau das Verhalten, das es vor dem Bedienelement gab, und das, was ein Client bekommt, der nichts sendet |
| **Approve everything** | Stehende Zustimmung für diese Unterhaltung: Jeder gegatete Aufruf wird gewährt, ohne zu parken — und jeder schreibt trotzdem seine Zeile |
| **Ask about everything** | Gate jedes Tool, das der Agent erreichen kann, einschließlich derer, die der Spec ungegatet ließ, und derer, die keiner Capability gehören |

Vier Dinge machen es zu einer Sitzungseinstellung statt zu einem Loch im Modell:

- **Es wird abgelehnt, nie herabgestuft.** Einem Aufrufer, der nicht verzichten
  darf, wird das gesagt; der Turn läuft nicht still dem Spec folgend weiter, denn
  wer glaubt, die Fragen abgeschaltet zu haben, und dann einen geparkten Run
  vorfindet, dem wurde das Gegenteil dessen gesagt, was geschehen ist. Die Prüfung
  liegt in `AgentRunnerService.prepare`, dem einen Trichter, den ein frischer und
  ein fortgesetzter Run teilen, statt an einem Socket, den ein Aufrufer vergessen
  könnte.
- **Ein Verzicht braucht `approvals:decide` und die Erlaubnis der Organisation.**
  Eine stehende Zustimmung _ist_ die Entscheidung, zu deren Festhalten die
  Freigabe-Queue existiert, und `member` und `builder` führen Agents aus, ohne sie
  zu halten — ohne die Berechtigungsprüfung gewährte sich also der alltägliche
  Chat-Nutzer mit einem Klick die Autorität, die die API ihm einen Endpunkt weiter
  verweigert. Der eigene Schalter der Organisation (`chat_may_waive_approvals`,
  für neue Organisationen voreingestellt ein, geändert von jemandem, der `approvals:decide` hält) ist die
  Obergrenze: Ohne ihn ist das bewusste Gate eines Builders auf `send_email` in
  jeder Unterhaltung einen Klick von nichts entfernt und das Modell je Tool nur
  beratend.
- **Kein Channel heißt weiterhin nein.** Nur eine Web-Chat-Sitzung darf
  verzichten. Ein Zeitplan, ein Webhook, ein Embed und ein Channel werden alle
  abgelehnt, denn `ApprovalGate` lehnt einen Run, bei dem niemand zu fragen ist,
  bereits ab, und eine stehende Zustimmung darf nicht der Weg daran vorbei werden.
- **Jeder verzichtete Aufruf wird trotzdem festgehalten.** Die Zeile wird als
  `approved` geschrieben, benennt das Konto, das zugestimmt hat, mit
  `decided_via = "standing"` — und der Freigabe-Datensatz sagt das in Worten neben
  dem Namen. Die Zeile wegzulassen würde einen verzichteten Run von einem Agent
  ununterscheidbar machen, der nie gegatet war, und das wäre diese ganze Spur, die
  still aufhört, eine zu sein. Niemand hat diese Argumente gelesen, bevor sie
  liefen; die Zeile ist der Ort, an dem sie jemand hinterher liest.

**Ask about everything verschärft die Regeln der Sitzung.** Für die Auswahl dieses
strengeren Modus ist keine besondere Berechtigung nötig. Der Modus erfasst MCP-Aufrufe,
die der Runner ausführt, obwohl diese Tools nicht von Capabilities deklariert
werden. Erlaubnislisten an Verbindung und Agent-Bindung bestimmen, welche
MCP-Tools verfügbar sind; was sie für eine Person zurückhält, ist die eigene
`approval`-Regel der Bindung - `writes` oder `all` -, die der Standardmodus
**Follow the agent** wie die einer Capability anwendet (siehe
[welche Aufrufe auf eine Person warten](mcp.md#which-calls-wait-for-a-person)).

### Eine Freigabe innerhalb eines Sprachanrufs { #an-approval-inside-a-voice-call }

Ein [Sprachanruf](voice.md#approvals-in-a-call) kann nicht pausieren und auf die
Queue warten, also werden seine Freigaben im Anruf entschieden, von der Person
im Anruf:

- Die Anfrage wird sofort in die Queue geschrieben, und der Anruf zeigt das
  Tool, die exakten Argumente, mit denen es laufen wird, und was vom Anrufer
  verstanden wurde. Freigeben führt diese Argumente aus, einmal.
- Entschieden wird sie **nur über den Anruf**, und nur von einem Anrufer, der
  `approvals:decide` hält. `POST /approvals/{id}` weist eine Anfrage ab, die zu
  einem laufenden Anruf gehört, und der Sweep für liegengebliebene Freigaben
  unten überspringt Voice-Runs.
- **60 Sekunden** ohne Entscheidung lassen sie ablaufen, ohne entscheidende
  Person - dasselbe null, das der Sweep schreibt. Ein Anruf, der aus
  irgendeinem Grund endet, lässt alles ablaufen, worauf er noch gewartet hat.
- Ein Mitglied ohne `approvals:decide` bekommt das klar gesagt, und das Tool
  wird abgelehnt. Ein Besucher auf einem Widget oder einer gehosteten Seite sieht
  nie ein Tool, das eine Freigabe braucht: Es wird aus seinem Anruf
  weggelassen.

`approval_mode` folgt dem Spec des Agents, wie im Text-Chat, und jede
Entscheidung erreicht den Audit-Trail wie jede andere.

### Eine Entscheidung, die niemand trifft { #a-decision-nobody-makes }

Eine Freigabe wartet auf eine Person, und manche warten ewig: Die prüfende Person
ist gegangen, das Tool wurde an einem Freitag angefragt, niemand wusste, dass die
Entscheidung ihm zufiel. Nichts in einem Anfragepfad kann eine beenden — die ganze
Prämisse ist, dass keine Anfrage kommt —, also verweigert ein stündlicher Sweep
durch Zeitablauf alles, was jenseits von `APPROVAL_EXPIRY_HOURS` noch aussteht
(drei Tage voreingestellt, was ein Wochenende überspannt).

!!! warning "Es geht um den Run, nicht um die Zeile"

    Eine ausstehende Freigabe hält ihren Run unbegrenzt in `awaiting_approval`:
    Arbeit, die weder fertig ist noch fertig werden wird.

Ein solcher Run steht in der Historie und im Alter der ältesten Wartezeit auf dem
Dashboard, also folgt der Sweep jedem abgelaufenen Aufruf hinab bis zum Run
dahinter und beendet ihn, `cancelled` — niemand kam zurück, und was er vor dem
Parken ausgegeben hat, bleibt stehen.

Drei Dinge, die er bewusst nicht tut:

- **Er setzt den Run nicht fort.** Ein _zurückgewiesener_ Aufruf wird durch
  Fortsetzen erledigt: Die Verweigerung wird nachgespielt und der Agent arbeitet
  bis zu einer Antwort weiter. Das ist eine Modellanfrage gegen die eigenen
  Schlüssel der Organisation, und eine solche nach Zeitplan zu stellen, für einen
  Run, auf den niemand wartet, sind keine Kosten, die ungefragt entstehen sollen.
- **Er beendet keinen Run, dessen Aufruf noch in seinem Fenster liegt.** Ein Run
  parkt auf allen seinen offenen Aufrufen zugleich, also wird er erst beendet,
  wenn keiner von ihnen mehr aussteht.
- **Er benennt keine entscheidende Person.** `decided_by_user_id` bleibt null und
  ebenso der Akteur des Audit-Eintrags, denn das ist die Tatsache, die festgehalten
  wird. Null bedeutet dort die Plattform nach Zeitplan, und nichts anderes kann
  eine erzeugen.

Das ist der einzige Lesevorgang in der Codebasis, der jede Organisation
überschreitet, aus dem Grund, dass ein Zeitplan keinen Tenant hat, auf den er
begrenzt sein könnte. Jeder Schreibvorgang, den er macht, liegt weiterhin in der
Organisation der jeweiligen Zeile.

### Ein Run, dessen Prozess gestorben ist { #a-run-whose-process-died }

Der andere Zustand, den nichts im Prozess je auflösen wird.

Die Zeile eines Runs wird als `running` committet, bevor sein Modell aufgerufen
wird ([#12][12-issue]), sodass ein Worker, der mitten im Run getötet wird — OOM,
ein Deploy, der nicht drainiert —, eine dauerhafte Zeile hinterlässt, die nichts
mehr zu Ende bringt: für immer in **Aktivität** (Aktivität), und sie blockiert jeden Zeitplan, für
dessen Trigger sie der verknüpfte Run war.

Während sein Modell arbeitet, stempelt der Prozess, der einen Run ausführt, ihn
alle `RUN_HEARTBEAT_SECONDS` als lebendig. Ein minütlicher Sweep beendet alles,
was `STALE_RUN_REAPED_AFTER_SECONDS` lang ohne Lebenszeichen noch `running` ist —
zwei Minuten voreingestellt, null schaltet es ab — als **`failed`** und sagt es
den offenen Konsolen, sodass ein Chat nicht mehr "Working" über einem Zug zeigt,
den niemand beantwortet, und anbietet, erneut zu fragen. Den Run eines
Sprachanrufs überlässt er dem Reaper der Anrufe, der den Anruf auflegt. Niemand hat diesen Run gestoppt, die
Infrastruktur hat es getan, und ein Betreiber, der die Run-Historie nach
Problemen filtert, ist genau der, der das sehen sollte.

Der Fehler auf der Zeile ist der eigene Satz des Sweeps. Der Prozess, der mehr
wusste, ist gestorben.

Das Alter eines Runs ist hier sein **letzter Übergang**, nicht sein erster Start.
Ein Fortsetzen behält das ursprüngliche `started_at` — der Run spannt sich über
beide Segmente —, sodass ein Run, der Tage nach dem Parken freigegeben wurde, ab
dem Moment altert, in dem sein Nachspielen begann, statt ab einem Start, der ihn
mitten im Nachspielen abernten ließe.

Die Obergrenze muss so oder so nicht exakt sein, denn ein lebender Run, den der
Sweep trotzdem umschaltet, schaltet sich selbst zurück: Sein eigener terminaler
Schreibvorgang landet später und gewinnt.

Was ein abgeernteter Run nicht wiederherstellen kann, sind seine **Ausgaben**. Das
Ledger ist mit dem Prozess gestorben, also behält die Zeile die Nullen, mit denen
sie geöffnet wurde, statt eine Zahl zu bekommen, die jemand gegen eine Rechnung
abgleichen würde.

Und niemand bekommt Post. Die Fehlschlag-Benachrichtigung reist auf `finish` mit,
das den Agent und seinen Spec zur Hand hat; ein Sweep hat weder noch.

[12-issue]: https://github.com/vstorm-co/agenticos/issues/12

### Eine Freigabe innerhalb einer Delegation { #an-approval-inside-a-delegation }

Die Tools eines Delegates werden vom eigenen Spec des Delegates gegatet, und er
erreicht dieselbe Queue, an der der Aufrufer des Parents ohnehin schon wartet —
ein Spezialist, der eine Person braucht, braucht die Person, die dasteht.

Der Eintrag nennt das Tool **des Delegates** und die Argumente, die es
vorgeschlagen hat, denn das eigene Gate des Delegates hat ihn geschrieben. Und er
nennt, **welcher Delegate ihn aufruft**.

Ohne diesen letzten Teil sagt die Queue `send_email`, ohne zu sagen, ob der Agent,
mit dem jemand spricht, oder ein Spezialist namens `researcher` sie sendet. Das ist
eine Queue, die Menschen blind freigeben — und in einer Delegation ist das, was
freigegeben wird, oft folgenschwerer als der Agent, mit dem die prüfende Person zu
tun zu haben glaubt.

Diesen Delegate zu löschen tilgt nicht den Datensatz dessen, wozu er autorisiert
war: Die Zeile behält den Namen des Delegates und lässt nur die Verknüpfung zu
seinem nun verschwundenen Agent fallen. Das gilt auch, wenn die Löschung landet,
während der Run noch geparkt ist, bevor die Freigabe-Zeile geschrieben wurde — der
aufgeschobene Schreibvorgang
([#169](https://github.com/vstorm-co/agenticos/issues/169)) löst die noch
vorhandenen Delegates auf und schreibt für einen verschwundenen eine null-Id,
genau das, was ein Löschen nach dem Entstehen der Zeile getan hätte.

Was der Run des Parents tut, ist parken, statt etwas in die Hand zu bekommen, das
wie eine fertige Delegation aussieht. Das ist es wert, gesagt zu werden, weil es
früher anders war: Jeder hier gebaute Agent deklariert einen Output-Typ, der einen
Run mit seinen geparkten Aufrufen als _Output_ enden lässt, statt auszulösen, und
die Delegationsbibliothek serialisierte dieses Objekt und reichte dem Modell des
Parents `{"calls": [], "approvals": [...]}` als Bericht des Spezialisten, Task als
abgeschlossen markiert. Es war der Standardpfad und kein Sonderfall, und es ist in
der gepinnten Version behoben.

Sie freizugeben **setzt den Delegate fort**, statt erneut zu delegieren. Der
geparkte Zustand ist ein Baum — eine Ebene je Agent, jede mit ihrer eigenen
Unterhaltung und ihren eigenen geparkten Aufrufen —, sodass die Freigabe den
ausgesetzten Delegate dort fortsetzt, wo er stehen blieb, mit dem Urteil an dem
Aufruf, den die prüfende Person tatsächlich gesehen hat. Der `task`-Aufruf des
Parents wird nachgespielt, und die Delegation findet die Stelle, die sie verlassen
hat. Ein Spezialist innerhalb eines Delegates verhält sich genauso, eine Ebene
tiefer.

Das zählt, weil die Alternative kein langsameres Fortsetzen ist, sondern eine
andere Antwort. Die Delegation neu auszuführen würde die Unterhaltung des
Delegates bei null beginnen und sein Modell beim zweiten Mal ein anderes Tool
aufrufen lassen, sodass das, was eine prüfende Person freigegeben hat, nicht das
wäre, was ausgeführt wird.

Was der Delegate bereits ausgegeben hatte, reist mit seiner Stelle mit, sodass die
Zeile, die geschrieben wird, wenn die Delegation endlich endet, alles davon
abdeckt — siehe
[wie ein delegierter Run festgehalten wird](#what-a-delegated-run-is-recorded-as).
Beide Hälften des Baums werden je Delegation statt je Run gehalten, und das ist
es, was einen Spezialisten drei Ebenen tiefer parken und trotzdem seinem eigenen
Agent zurechnen lässt. Die Ausgaben werden selbst dann behalten, wenn die _Stelle_
des Delegates es nicht konnte — die Message-Historie der Bibliothek ist Telemetrie
nach bestem Bemühen, und eine von vorn ausgeführte Delegation hat trotzdem
ausgegeben, was sie ausgegeben hat.

!!! warning "MCP-Freigaben werden an der Bindung eingestellt"

    Die Freigabeeinstellung einer Capability deckt MCP-Tools nicht ab; die
    `approval`-Regel der Bindung tut es, auf jeder Oberfläche, auf der der Agent
    läuft. `writes` hält die Tools zurück, die der Server nicht als schreibgeschützt
    markiert - das ist die Angabe des Servers selbst, wählen Sie für einen Server
    außerhalb Ihrer Kontrolle also `all`. Im Webchat erfasst **Ask about
    everything** (`ask_all`) auch den Rest; die strengere Einstellung einer
    Chatsitzung gilt nicht automatisch für einen Zeitplan oder einen anderen
    Zugang. Beim Modellanbieter ausgeführte Tools durchlaufen das lokale
    Freigabe-Gate nicht.

### Ein Workflow-Schritt, der eine Person fragt { #workflow-step-approvals }

Ein Workflow fragt mit einem [`human.approval`](reference/workflow-nodes.md#human-approval)-Schritt
statt mit einem abgesicherten Tool: Der Schritt schreibt seine eigene Anfrage -
einen Titel, die ihm übergebenen Details, wer entscheiden darf und bis wann -, und
sein Run wartet darauf. Der Tab **Approvals** listet sie über den Tool-Aufrufen,
und eine Entscheidung weckt den Run, der nach der Antwort weitergeht. Eine
Entscheidung ist endgültig und wird mit dem Gefragten im Audit-Trail festgehalten und bleibt unter **Decided** neben dem
Protokoll der Tool-Aufrufe aufgelistet.
Einen Schritt, der seine Freigebenden nennt, können nur sie entscheiden. Eine
Anfrage, die niemand rechtzeitig entscheidet, läuft ab, und ein abgebrochener Run
bricht seine Anfragen ab.

Ein Schritt mit `editable` zeigt der freigebenden Person auch einen Vorschlag,
etwa eine vorbereitete Antwort, den sie vor der Freigabe ändern darf. Der
Audit-Eintrag hält fest, ob der Schritt editierbar war, ob die Person ihn geändert
hat (`edited`) und wie lang der Vorschlag und der freigegebene Text sind
(`proposal_length`, `final_length`), nie den Text selbst. Der Text liegt im
Klartext bei der Anfrage, bis die Historie des Runs gelöscht wird.

Die Anfragen werden auch gezählt. Die Stats-Karte eines Workflows und seine
Statistik-API melden je Schritt und je Tag, wie viele wie vorgeschlagen freigegeben,
bearbeitet, abgelehnt wurden oder abgelaufen sind, wie lange die Entscheidung dauerte
und wie stark die Änderungen den Text verändert haben, jede als Verhältnis von `0` bis
`1` (`edit_ratio`, auch im Audit-Eintrag); siehe
[Running a workflow](workflows.md#running-a-workflow). Die organisationsweite
Übersicht, `GET /api/v1/workflow-approvals/stats`, braucht `workflows:view` und
umfasst nur Workflows, die das Mitglied ansehen darf. Sie enthält weder Vorschlag noch
freigegebenen Text noch eine Zahl zu einzelnen Freigebenden: Sie zählt Anfragen, nicht
Personen.

### Artefakt-Einträge { #artifact-entries }

Eine Seite, die Tabellendaten liest, wird mit der Einwilligung ihres Besitzers
geteilt, und jeder Schritt davon hinterlässt einen Eintrag mit der Person, die ihn
getan hat. Keiner trägt einen Datensatz, einen Filterwert oder eine Zeile: Sie
halten Versionen, Fingerabdrücke, Anfrage-IDs und die Worte, die eine Person
bestätigt hat.

| Aktion | Aufgezeichnet, wenn |
| --- | --- |
| `artifact.published` | Eine Version veröffentlicht wurde, mit ihrer Nummer, dem Fingerabdruck ihrer Seitenabfragen und `actions`, den IDs der Workflows, die sie deklariert |
| `artifact.data_consented` | Der Besitzer die Daten einer Version bestätigt hat oder ihr automatisch zugestimmt wurde (`automatic`). Nennt die Version und den Fingerabdruck, `for_owner`, wenn ein Owner oder Admin für ihn bestätigt hat, und `via_grant`, wenn ein `edit_data`-Grant sie erteilt hat |
| `artifact.data_withdrawn` | Ein Editor die Seite daran gehindert hat, ihre Daten zu teilen; nennt die Version, die der Zielgruppe ausgeliefert wurde, ob die automatische Einwilligung an war, und wie viele `edit_data`-Grants es auf `edit` gesenkt hat |
| `artifact.auto_consent_changed` | Der Besitzer „Share new versions automatically“ ein- oder ausgeschaltet hat, mit dem Text, den er bestätigt hat |
| `artifact.edit_data_granted` | Der Besitzer oder ein Owner oder Admin einer Person oder Gruppe „Can edit, including data“ gegeben hat, mit dem Text, den er bestätigt hat |
| `artifact.owner_changed` | Eine Seite den Besitzer gewechselt hat: der alte und der neue Besitzer, wer es getan hat, und alle `edit_data`-Grants, die es entfernt hat |
| `artifact.share_requested` | Eine Anfrage gestellt wurde - eine `audience`-, `access`- oder `version`-Anfrage - von einer Person oder vom Run ihres Agents |
| `artifact.share_request_approved` und `artifact.share_request_declined` | Eine Anfrage entschieden wurde, und von wem |
| `organization.public_live_data_changed` | Ein Owner oder Admin **Public pages with live data** für die Organisation ein- oder ausgeschaltet hat, mit dem alten und dem neuen Wert |
| `artifact.public_data_consented` | Der Besitzer oder ein Owner oder Admin für ihn bestätigt hat, dass jeder mit dem öffentlichen Link der Seite und die Websites, die sie einbetten, ihre Daten lesen dürfen. Nennt die Version, den Fingerabdruck, die Tabellen und Spalten, die einbettenden Websites und ob ein Passwort gesetzt ist, sowie `for_owner`, wenn jemand für ihn bestätigt hat |
| `artifact.public_data_withdrawn` | Ein Bearbeiter diese Einwilligung zurückgezogen hat, angemeldet als er selbst (nicht imitierend), mit dem Fingerabdruck, den sie hielt |
| `artifact.public_data_voided` | Die Einwilligung gelöscht wurde, weil sich etwas darunter geändert hat, mit einem `reason`: `link_off`, `password_removed`, `origin_added`, `owner_changed`, `consent_withdrawn` oder `archived` |
| `artifact.action_started` | Ein Mitglied hat einen Workflow aus der Aktion einer Seite bestätigt und gestartet. Nennt die Seite, die Version, den Workflow und den Run, nie die Eingabewerte, die am Run stehen |

**Public pages with live data** ist eine Einstellung der Organisation, standardmäßig
aus, in den Einstellungen der Organisation neben dem Verzicht auf Freigaben. Sie
entscheidet, ob die Daten einer Seite Personen ohne Konto über einen öffentlichen Link
oder ein Embed erreichen dürfen. Anders als der Verzicht braucht sie `org:settings` -
Owner oder Admin - und muss angemeldet als man selbst geändert werden: Eine
imitierende Session wird abgelehnt. Die Seite braucht weiterhin die
[Einwilligung ihres Besitzers für öffentliche Daten](artifacts.md#live-data-on-a-public-link).
Das Ausschalten ist ein Kill-Switch: Öffentliche Seiten verlieren ihre Daten beim
nächsten Laden und behalten ihre Einwilligung, die wieder gilt, sobald der Schalter
wieder an ist.

Die Sichtbarkeit oder die Grants einer Seite zu erweitern, wird wie bei jeder
anderen Ressource als Einträge `resource.share`, `resource.unshare` und
`resource.visibility_changed` aufgezeichnet. Das Tool `share_view` gibt es nicht
mehr, und damit auch nicht die Freigabe, die es abgesichert hat.

## Alerts { #alerts }

Jeder Alert hier handelt von einem Run, auf den niemand schaut. Ein Chat-Run, der
an seinem Budget stoppt, sagt das auf dem Bildschirm; derselbe Run, von einer
Slack-Erwähnung, einem Zeitplan oder einem API-Aufruf gestartet, stoppt lautlos,
und das Erste, was irgendwer davon hört, ist jemand, der fragt, warum der Agent
still geworden ist.

### In-App, neben E-Mail { #in-app-alongside-email }

Jeder Alert in diesem Abschnitt schreibt zwei Dinge: die unten beschriebene
E-Mail, und eine Zeile an **der Glocke** in der [Konsole](console.md#the-bell)

- dem eigenen Postfach der Konsole, keine Kopie der Mail. Die Zeile _ist_ die
  In-App-Zustellung; nichts weiter muss gelingen, damit sie erscheint. E-Mail
  ist ein zweiter, unabhängig wiederholter Kanal desselben Schreibvorgangs,
  weshalb der eine fehlschlagen kann - eine unzustellbare Adresse, ein
  ausgefallenes SMTP-Relay - ohne dass der andere je davon erfährt.

Beide Kanäle werden unabhängig voneinander geschaltet, je Ereignis, unter
**Settings → Notifications** - eine Person kann die In-App-Zeile für
Freigaben behalten und ihre E-Mail abschalten, oder umgekehrt.

Anfragen zum Teilen einer Seite und Besitzerwechsel nennen die Seite und die Person, nie ihre Daten.

Dieselbe Seite
trägt auch jedes andere Ereignis, das das Postfach zustellt: einen Run, der

Ein `eval`-Run ist einer von vielen gemeinsam gemeldeten Runs einer Evaluation; er sendet keine einzelne Abschluss- oder Fehlermeldung.

unbeaufsichtigt fertig wird oder scheitert - das Ende einer Routine nach ihr
benannt und mit dem Anfang ihrer Antwort, für die Person, die sie eingerichtet
hat -, die Bitte, eine Capability einzuschalten, für den Besitzer des Agents, Vorschläge,
die die [Arbeit aus eigener Initiative](#proactive-work) für eine Person gemacht
hat, die Ingestion eines Dokuments, die
**scheitert**, die eigene Gesamtzahl eines Konnektor-Laufs, und die eigene
Ankündigung eines App-Admins - `POST /admin/announcements`, noch keine
Console-Seite - adressiert nach Organisation und, optional, nach Rolle, und
beschränkt auf einen oder beide Kanäle.

Ein Dokument, das sauber indexiert wurde, schreibt nichts, und das ist Absicht.
Früher schrieb es je eine Zeile, was im Normalfall - ein Ordner Dateien, auf
einmal hinzugefügt - eine Benachrichtigung pro Datei bedeutete, die besagte,
dass nichts schiefgegangen war, und die begrub, was gelesen werden musste. Wo
eine gewöhnliche Ingestion jetzt gemeldet wird, ist der eigene Status des
Dokuments in seiner Collection und, bei einem Konnektor-Lauf, die eine Zeile,
die `sync_completed` schreibt, wenn der gesamte Versuch fertig ist. Ein
Scheitern erreicht weiterhin den, der die Datei hochgeladen hat, oder die
Administratoren der Organisation, wenn es niemand war.

Die eine Ausnahme sind die weiter unten beschriebenen wöchentlichen und
monatlichen Usage-Reports, die auf dem Agent konfiguriert werden: Beide
teilen sich eine einzige, veraltete E-Mail-Einstellung, sodass das Abschalten
der E-Mail des einen Reports auch die des anderen abschaltet - die
In-App-Zeile jedes Reports schaltet sich weiterhin einzeln.

Anders als alles oben lässt sich ein Sicherheitsereignis oder eine
Konfigurationsänderung auf keinem der beiden Kanäle abschalten. Es erreicht
die eigenen Owner und Admins dieser Organisation - nicht das breitere
`admins`-Publikum von oben - und nie die App-Admins des Deployments, außer
die Aktion selbst hat keine Organisation, der sie zuzuordnen wäre; in diesem
Fall bekommt sie stattdessen jeder App-Admin. Nichts davon weitet aus, was
diese Seite dokumentiert: Es ist dasselbe Postfach, in dem die oben auf dem
Agent konfigurierten Alerts landen, und die Opt-out-Regel unten gilt
weiterhin für alles, was sich abschalten lässt.

Aber nicht unbegrenzt: Jedes ist auf zwanzig Schreibvorgänge pro Minute je
Akteur und Ereignistyp begrenzt, sodass ein Konto mit schnellen Änderungen
nicht jeden Admin überfluten kann - der Audit-Eintrag dahinter wird trotzdem
aufgezeichnet, auf dem Trail selbst ([Audit](#audit)), unabhängig davon, ob die
Benachrichtigung das Limit überstanden hat.

Jenseits dieser Grenze wird der Posteingang nicht einfach still. An die Stelle
des Rests tritt eine Meldung pro Akteur und Minute, die sagt, dass die Minute
voller war, als der Posteingang auflisten kann, und dass jedes dieser
Ereignisse auf dem Trail steht. Das zählt, weil sich diese beiden nicht
abschalten lassen: ein Akteur könnte das Kontingent sonst auf zwanzig harmlose
Änderungen verbrauchen und danach das eine tun, worauf es zu achten gilt, ohne
dass irgendetwas es sagt. Die Meldung trägt keine Zählung - zählen hieße, sie
bei jedem weiteren Ereignis neu zu schreiben, also genau die Flut, die die
Grenze verhindern soll.

Eine Zeile fällt neunzig Tage nach dem Schreiben aus dem Postfach, wenn sie
_gelesen_ ist, und ein Jahr danach unabhängig davon, ob sie je geöffnet
wurde - beides gezählt ab dem Schreiben, nie ab dem Lesen, sodass eine am Tag
vor ihrer oberen Grenze geöffnete Zeile mit jeder anderen so alten
zusammen verschwindet. Ein Sweep im Hintergrund, den niemand auslöst. Der
E-Mail-Sweep versendet keine Zeile, die diese Grenze bereits überschritten hat,
sodass ein nach langem Ausfall wiederanlaufender Worker keine Benachrichtigung
auf ihrem Weg in die Löschung verschicken kann.

Das [Leeren](console.md#the-bell) ist das andere und nicht dasselbe: Ein Mensch
nimmt eine Zeile sofort aus der eigenen Liste, und die Zeile selbst wird
behalten, bis dieser Sweep sie erreicht.

Was das übersteht, hängt davon ab, worum es in der Benachrichtigung ging. Ein
Sicherheitsereignis, eine Konfigurationsänderung und die eigene Ankündigung
eines Admins beginnen als Audit-Eintrag, und dieser Eintrag überlebt die Zeile,
die das Postfach gezeigt hat ([Audit](#audit)). Der Ausgang eines Runs, ein
Ingestion-Ergebnis und ein Nutzungsbericht schreiben keinen eigenen
Audit-Eintrag: Der Run, das Dokument und die Ausgaben, die sie beschreiben,
sind der Datensatz, und die Benachrichtigung ist nur, wie jemand davon erfahren
hat - die berechneten Zahlen eines periodischen Berichts, die nirgends außer in
der Benachrichtigung leben, sind das Einzige, was eine abgelaufene Zeile
mitnimmt.

### Ein Ergebnis, einmal erledigt { #one-outcome }

Eine wartende Freigabe, ein fehlgeschlagener Lauf und ein Fund einer Beobachtung sind Dinge, auf die
jemand reagieren muss, also ist jedes ein **Ergebnis**: ein Eintrag pro Person und Ursache, egal wie
viele Stellen davon berichten (#2575). Die Karte über dem Chat, aus dem es stammt, die Zeile an der
Glocke, der Push und die E-Mail sind Zustellungen dieses einen Ergebnisses, nicht vier Einträge.

Ein Ergebnis wird einmal bestätigt, gelöst oder stummgeschaltet, von der Chat-Karte oder der Glocke
aus, und jede Stelle folgt. Bestätigen markiert es als gelesen und storniert Push und E-Mail, die noch
auf den Versand warten; Lösen und Stummschalten räumen es zusätzlich weg. Eine Zustellung, die ein Worker
schon hält, wird kurz vor dem Senden erneut geprüft, sodass ein dazwischen erledigtes nicht hinausgeht.
`POST /outcomes/{id}/settle` nimmt die Aktion entgegen, und `GET /outcomes` listet die eigenen
Ergebnisse des Aufrufers, optional für einen Chat.

Die Entscheidung über die letzte offene Freigabe eines Laufs erledigt sein Ergebnis. Eine stummgeschaltete Ursache bleibt stumm. Bei anderen Benachrichtigungen kann eine gelöste oder bestätigte Ursache bei ihrer Rückkehr wieder geöffnet werden; ein wiederholter Fund behält seinen bisherigen Zustand.

Jeder Fund einer Beobachtung, auch ohne dringende Glockenmeldung, teilt einen Fall mit seinem Inbox-Eintrag und seiner Karte im ursprünglichen Chat. **Erledigt** löst diesen Fall; Ablehnen schaltet ihn stumm. Beide Aktionen stornieren ausstehende externe Zustellungen. Bestätigen stoppt Benachrichtigungen und weitere Meldungen im Briefing, lässt den Fund aber mit **Zur Kenntnis genommen** in der Inbox bearbeitbar. Das Löschen einer Beobachtung erledigt ihre offenen Fälle, bevor ihre Funde gelöscht werden. Entscheidungen gelten nur für die besitzende Person innerhalb derselben Organisation.

Nur ausdrückliches **Wiederherstellen** eines abgelehnten Fundes öffnet denselben Fall erneut. Alte Benachrichtigungen und Zustellungen werden nicht wiederholt. Die Migration `0264_finding_outcome_state.py` verknüpft vorhandene Funde mit ihren Fällen, erstellt Fälle für nicht dringende Funde, bewahrt IDs und frühere Inbox-Entscheidungen und storniert überholte Zustellungen. Ein Downgrade entfernt die Verknüpfung, kann aber weder Entscheidungen rückgängig machen noch vom Anbieter angenommene Nachrichten zurückrufen; bewahren Sie ein Backup vor dem Upgrade auf.

### Auf dem Agent konfiguriert { #configured-on-the-agent }

Wer von einem Agent hört, ist Teil des Specs des Agents, unter **Limits →
Alerts**. Ein deploymentweites Publikum machte den lauten Agent und den, den
niemand verpassen darf, zu derselben Einstellung, sodass der einzige Weg, den
ersten leiser zu stellen, war, für den zweiten taub zu werden.

| Alert | Löst aus, wenn | Voreingestelltes Publikum |
| --- | --- | --- |
| **Budget** | dieser Agent sein eigenes monatliches Cap erreicht hat | die Admins und der Besitzer des Agents |
| **Freigaben** | ein Tool-Aufruf geparkt hat | wer den Run gestartet hat, plus die Admins |
| **Nutzung** | wöchentlich und monatlich, was dieser Agent ausgegeben hat | aus |

Das Log eines Workflows hat eigene Alerts, die auf der Logs-Seite des Workflows gesetzt
werden statt in einer Spec; siehe [Alerts](workflows.md#log-alerts). Sie lösen aus, wenn
die passenden Zeilen einer Regel ihren Schwellenwert in ihrem Fenster erreichen, und
gehen an den Eigentümer des Workflows, sofern die Regel nichts anderes sagt. Es gelten
dieselben Glocken- und E-Mail-Einstellungen wie im Rest dieses Abschnitts, unter
**Workflow log alerts**.

Ein Publikum ist eine Liste von Rollen, nicht von Adressen:

| Publikum | Löst sich auf zu |
| --- | --- |
| `admins` | die Owner und Admins der Organisation, **plus die App-Admins des Deployments** |
| `owner` | der Besitzer des Agents |
| `initiator` | wer den Run gestartet hat; niemand, bei einem Run, den ein Zeitplan begonnen hat |
| `chosen` | genau die daneben benannten Mitglieder |

Rollen statt Adressen, weil ein Spec in das Repository eines Kunden exportiert
wird und die Menschen darin überlebt: `admins` bedeutet nach einer Umstrukturierung
weiterhin die richtigen Personen, und es bedeutet sie in derjenigen Organisation,
in die der Spec importiert wird. Ein benanntes Mitglied, das gegangen ist, steuert
nichts bei, statt auszulösen — eine Freigabe-Queue darf nicht verstummen, weil eine
Id sich nicht mehr auflöst.

### Ein Alert verlinkt dorthin, wo die Entscheidung ist { #an-alert-links-to-where-the-decision-is }

**Freigabe-Post öffnet die Queue** — `/runs?tab=approvals`, den Approvals-Tab von
**Aktivität** (Aktivität), die einzige Oberfläche, die Approve und Reject trägt. Früher öffnete sie
`/agents/{id}`, den Builder: ein Satz Prosa darüber, dass Tool-Aufrufe eine Queue
erreichen, und keine Queue. Also landete die eine E-Mail, deren ganzer Zweck
_jemand muss jetzt entscheiden_ ist, eine Suche von der Entscheidung entfernt,
während der geparkte Run auf `ApprovalService.expire_stale` zualterte (#935).
Für den Tab gab es keine URL, bis #934 sie in `?tab=` legte.

Sie nennt den Run bewusst **nicht** mit `&run=`, obwohl der Alert einen hält: Die
Entscheidungs-Bedienelemente sitzen auf der Zeile in der Queue, und unterhalb von
`lg` ersetzt ein fokussierter Run die Liste — was sie vor dem Leser verbergen
würde, der am ehesten am Telefon sitzt.

Budget-Post öffnet den Agent, und das ist das richtige Ziel dafür: Das Cap, über
das sie berichtet, wird dort bearbeitet.

### Der Freigabe-Alert sind zwei E-Mails { #the-approval-alert-is-two-emails }

`approvals:decide` gehört zu `owner`, `admin` und `operator`. Das voreingestellte
Publikum für einen geparkten Aufruf schließt ein, wer den Run gestartet hat, und
ein Builder, der seinen eigenen Agent aus dem Chat startet, ist der gewöhnliche
Initiator — der Alert erreichte also regelmäßig jemanden, den die Plattform
ablehnen würde. Sie bekamen **„waiting on your approval"** mit einer Schaltfläche
**Review the request**, folgten ihr, und **Aktivität** (Aktivität) zeichnete überhaupt keinen
Approvals-Tab: Die Ablehnung kam als abwesender Tab an statt als Satz.

Das Publikum wird also nach der Berechtigung aufgeteilt, statt darauf beschnitten:

| Empfänger hält | Bekommt |
| --- | --- |
| `approvals:decide` | die Anfrage, mit dem Link zur Queue |
| irgendetwas Geringeres | die _Tatsache_: Der Run ist angehalten, nicht gescheitert, ihn freizugeben ist Sache eines Owners, Admins oder Operators, und von ihnen wird nichts verlangt |

Zu beschneiden würde stattdessen die eine Person, die definitiv auf den Run
wartet — die Person, die ihn gestartet hat —, nichts darüber hören lassen, warum
er gestoppt hat.

Die zweite E-Mail trägt **keinen Link**, und das ist gewollt und nicht
unfertig. Dass `agents:view` eine Rollenberechtigung ist, macht keinen einzelnen
Agent erreichbar: Der Zugriff auf einen Agent wird je Ressource aufgelöst, sodass
ein `chosen`-Empfänger ohne Grant auf einen privaten Agent eine zweite
Handlungsaufforderung bekäme, die die Plattform ablehnt — derselbe Defekt an einer
neuen Stelle. Von diesem Leser wird nichts verlangt, also wird ihm nichts
angeboten. Ebenso wenig behauptet sie, irgendwer sonst sei informiert worden: Ein
Publikum aus einer Person ohne Entscheidungsrecht heißt, dass niemand, der
entscheiden kann, überhaupt angemailt wurde, und ein Satz, der etwas anderes
verspricht, ließe sie auf jemanden warten, der nie davon gehört hat.

Welche Rollen entscheiden, wird am Berechtigungskatalog abgelesen und nicht
daneben aufgelistet: Eine Rolle, die `approvals:decide` gewinnt oder verliert,
darf das Routing nicht zurücklassen, und das ist derselbe Defekt eine Ebene höher.

### Jeder Link sagt, um welche Organisation es geht { #every-link-says-which-organization-it-is-about }

Die Konsole handelt in derjenigen Organisation, die der Leser zuletzt benutzt hat:
`apiClient` stempelt `X-Organization-Id` aus einer je Browser gespeicherten
Auswahl. Jede Alert-URL trug früher keine, sodass jemand in zwei Organisationen,
der zuletzt in Globex gearbeitet hatte, den Freigabe-Alert für einen Run in Acme
öffnete und **Globex'** Queue las — sehr wahrscheinlich leer, und zu lesen als
_es wartet nichts_ über einen Run, der geparkt ist und auf
`ApprovalService.expire_stale` zualtert. Die Agent-Links waren leiser falsch:
`/agents/{id}` unter der falschen Organisation ist eine Ablehnung für einen Agent,
den der Leser wirklich sehen kann, einen Wechsel entfernt.

Also trägt jeder Link `org=<id>`, an einer Stelle gebaut —
`NotificationService._link`, das den Trenner aus dem Pfad wählt, weil der
Freigabe-Link bereits `?tab=approvals` trägt — statt an jeder der vier
Aufrufstellen, und die Konsole übernimmt sie genau so, wie sie die Id in
`/orgs/{id}` übernimmt: Eine Seite, die eine Organisation nennt, _ist_ diese
Organisation. Die Übernahme ist ein Layout-Effekt in `ActiveOrgGuard`, vor dem
Zurücksetzen des Tenant-Caches und vor den eigenen Abfragen der Seite, sodass die
erste Anfrage der Seite bereits den richtigen Tenant trägt. Der Pfad steht über
dem Parameter — ein Link sagt, um welche Organisation ein Alert ging, und kann
niemanden von der Seite wegbewegen, auf der er gerade steht.

Einem Leser, der diese Organisation inzwischen verlassen hat, wird das gesagt,
statt ihn still zu verschieben: Die Ablehnung nennt den Link als Grund, denn im
Stillen umgeschaltet zu werden ist der Weg, auf dem die Seite einer anderen
Organisation zur Antwort auf den Alert wird. Sie kann die Organisation nicht
nennen — der Leser ist kein Mitglied, also steht er nicht in ihrer Liste.

### Zwei Regeln, die nicht verhandelbar sind { #two-rules-that-are-not-negotiable }

**Ein Opt-out je Person zieht immer nur ab.** Die eigenen Schalter jedes
Empfängers unter **Settings → Notifications** werden zuletzt angewendet. Ein Agent
kann entscheiden, dass die Admins von ihm hören sollen; ein Admin kann trotzdem
entscheiden, dass er keine Budget-Post will. Nichts, was der Autor eines Agents
schreibt, verpflichtet jemanden in ein Postfach.

**Das Cap der Organisation ignoriert den Spec vollständig.** Dieses Limit stoppt
jeden Agent in der Organisation, und der Autor eines Agents kann es nicht
anheben, also geht sein Alert an die Administratoren, was auch immer irgendein
Agent verlangt. Ein Agent kann kein Limit stummschalten, das er nicht
kontrolliert.

### Stille bedeutet etwas { #silence-is-meaningful }

Eine Organisation, die nichts ausgeführt hat, bekommt keinen Bericht. Ein
wöchentliches „0 Runs, 0,00 $" ist der Bericht, den Menschen in einen Ordner
filtern, und dann landet der, auf den es ankam, ebenfalls dort.

Die Zahl in diesem Bericht sind die Ausgaben der Organisation über das Fenster —
dieselbe Arithmetik, mit der das Cap durchgesetzt wird, Ingestion eingeschlossen
und delegierte Runs einmal gezählt. Ein Bericht, dessen Summe dem Limit
widerspricht, das die Plattform durchsetzt, ist schlimmer als kein Bericht, denn
beide Zahlen sehen maßgeblich aus.

Das Senden blockiert nie und löst nie in den Aufrufer hinein aus: Ein Run, der
bereits geendet hat, darf nicht noch einmal scheitern, weil SMTP ausgefallen war.

## Bezahlte Evaluierung { #ai-assistance }

Bewertung, Testfallgenerierung und Verbesserung sind ausdrücklich gestartete, bezahlte Aktionen. Modell und Anweisungen gehören zum Dataset: **Bewertung** konfiguriert den Bewerter, **Einstellungen** Generierung und Verbesserung. Vor bezahlter Arbeit zeigt die Konsole eine Kostenschätzung.

Es gibt keinen organisationsweiten KI-Schalter und keine organisationsweite Modellvorgabe für Bewerter. Der Preis wählt kein vermeintlich stärkeres Modell. Ein automatisch erstellter Testsatz beginnt mit dem Modell des Agenten; ein unabhängiger Bewerter wird ausdrücklich am Dataset gewählt.

Berechtigungen, Modellzugangsdaten, Organisationsbudgets und Run-Limits gelten weiterhin. Jeder Aufruf wird gemessen und in Aktivität erfasst. Wartende Aufgaben behalten ihre Startkonfiguration; Änderungen gelten für spätere Arbeit. Historische Helferzeilen bleiben für Zuordnung und Budgetprüfung erhalten.

## Übergangene Promotionen { #promotion-overrides }

Eine Umgebung, die von [Eval-Suites bewacht](environments.md#guarding-a-promotion-with-evals)
wird, übernimmt eine Version erst, wenn sie auf ihr bestehen. An ihnen vorbei zu
promoten ist ein **Übergehen**: Es braucht `promotions:override` - Owner und
Admins haben es, und eine eigene Rolle kann es bekommen - von einer Person, die
als sie selbst angemeldet ist, nicht von einem API-Schlüssel oder einem
Administrator, der sie imitiert, und eine Begründung von mindestens zehn
Zeichen. Es wird im [Audit-Trail](#audit) als
`agent.environment_promotion_overridden` verzeichnet, mit der Begründung und
dem Ergebnis jedes Wächters, an der Version in der Historie gezeigt und an den
Eigentümer jedes nicht bestandenen Wächters geschickt.

Zu ändern, welche Suites eine Umgebung bewachen, die Festlegung eines Wächters
zu verschieben oder abzuschwächen, was ein Wächter prüft, braucht dieselbe
Berechtigung; jede dieser Änderungen, und eine strengere Schwelle, wird als
`agent.environment_guards_changed` verzeichnet. Eine Änderung an der Bewertung
eines Datasets wird als `dataset.scoring_changed` verzeichnet, mit den Namen
der geänderten Teile statt ihrem Inhalt, das Anlegen seines Judges als
`dataset.judge_created`.

## Audit { #audit }

Aktionen, die den Zugriff ändern oder Geld ausgeben, werden mit einem Akteur
festgehalten, und ein Kontext ohne Subjekt **löst aus**, statt die Abwesenheit
weiterreisen zu lassen. Ein Eintrag, der niemanden nennt, bedeutet also genau zwei
Dinge, und die `action` sagt welches: den Ablauf-Sweep für Freigaben und einen
Betreiberbefehl an der Shell des Deployments.

Zugangsdaten an eine Sammlung zu binden ist eine dieser Aktionen.
`sync_source`-Einträge halten das Erstellen, Klonen, Umhängen und Löschen einer
Quelle fest, denn die Zeile entscheidet, wer am Ende lesen kann, was sie
aufnimmt
([Dateiverarbeitung](file-processing.md#who-ends-up-able-to-read-what-a-source-ingested)).

Ein privilegierter **Massen-Lesevorgang** wird ebenfalls festgehalten. Jeder
CSV-Export schreibt einen Eintrag `runs.export`, `approvals.export` oder
`spend.export`, der das Fenster und die Zeilenzahl nennt — wer die ganze Tabelle
vom Bildschirm genommen hat, ist eine Frage, die jetzt billig zu beantworten und
später unmöglich zu rekonstruieren ist.

Der Schreibvorgang teilt sich die Transaktion der handelnden Anfrage, also
scheitert er geschlossen: Ein Eintrag, der sich nicht festhalten lässt, rollt die
Aktion zurück, die er beschreibt, statt eine privilegierte Mutation unauditiert
landen zu lassen.

`audit:read` gatet das Lesen. Die Umgehung eines App-Admins ist genau das, wozu
die Spur existiert, um sie zur Verantwortung zu ziehen.

Die Spur hält auch sich selbst ehrlich. Jeder Eintrag reiht sich in eine
Hash-Kette je Organisation ein — er trägt einen Hash über seinen eigenen Inhalt,
in den der Hash des vorherigen Eintrags eingeflochten ist — sodass das Ändern,
Umordnen oder Einfügen eines Eintrags, oder das Löschen eines aus der Mitte, jeden
nachfolgenden Hash auseinanderlaufen lässt.

`agenticos cmd audit-verify` läuft jede Kette ab, berechnet die Hashes neu und
nennt den ersten Eintrag, der nicht mehr passt; ohne Argument prüft es jede Kette,
auch die deploymentweite, die mandantenlose Aktionen hält — eine Änderung der
Deployment-Einstellungen, eine Impersonierung, App-Admin-Benutzerverwaltung — und
endet mit einem Exit-Code ungleich null, wenn eine Kette fehlschlägt. Das ist
**Erkennung, keine Verhinderung** — ein Operator mit Zugriff auf die Datenbank kann
eine Zeile weiterhin umschreiben und jeden nachfolgenden Hash neu berechnen — ein
sauberer Lauf belegt also, dass niemand manipuliert hat, der nicht auch die Kette
neu geschmiedet hat, kein Beweis, dass die Zeilen unveränderlich sind.

Zwei Löschungen kann die Kette von sich aus nicht erkennen, weil die verbleibenden
Zeilen intern konsistent bleiben: das Abschneiden der neuesten Einträge einer Kette
und das vollständige Löschen der Kette einer Organisation. Diese fängt stattdessen
ein **Checkpoint** ab — eine organisationsweite Höchstmarke, die `record_audit` neben
jedem Eintrag vorrückt, unter einem Datenbank-Trigger, der ihr Zurückgehen oder
Löschen verweigert. `audit-verify` meldet eine Kette, deren Kopf hinter ihrem
Checkpoint liegt, oder einen Checkpoint, dessen Kette weg ist.

Was dieser Trigger abdeckt, gehört genau gesagt, weil man leicht mehr hineinliest. Er
schließt den gewöhnlichen Schreibpfad — einen App-Administrator, der durch das
Produkt handelt, und einen Fehler in diesem Code — also das Bedrohungsmodell, für das
diese Spur geschrieben ist.

Gegen jemanden mit den Zugangsdaten der Datenbank selbst ist er keine Kontrolle. Die
Anwendung und ihre Migrationen verbinden sich mit derselben Rolle, und dieser Rolle
gehört die Checkpoint-Tabelle: sie kann den Trigger entfernen, und ein `TRUNCATE`
leert die Tabelle, ohne einen zeilenbezogenen Delete-Trigger überhaupt auszulösen.
Ein Superuser kann beides. Das zu schließen erfordert die Höchstmarke dort, wo die
Rollen dieser Datenbank nicht hinreichen — in einem Append-only- oder
Object-Lock-Speicher außerhalb —, was eine geplante Folgearbeit bleibt.

Zwei auditierte Schreibvorgänge für eine Organisation können die Kette nicht
aufspalten: jeder hängt unter einer organisationsbezogenen Sperre an, sodass sie
sich zu einer einzigen Linie serialisieren, statt beide denselben Kopf zu
verlängern.

Eine **impersonierte** Aktion nennt beide. Wenn ein App-Admin als ein anderes
Konto handelt, trägt das Access-Token den Administrator als `act`-Claim; jeder
Eintrag, den diese Anfrage festhält, behält `actor_user_id` als das Konto, als das
gehandelt wird, und fügt `impersonator_user_id` hinzu — den Administrator
dahinter. So löst sich „wer hat die Unterhaltung dieses Kunden gelesen" auf eine
Person auf, selbst wenn die Aktion als die des Kunden festgehalten wurde. Bei
einer gewöhnlichen Anfrage ist es null, wo niemand als jemand anderes handelt, und
nichts wird nachgetragen: Ob eine vergangene Aktion impersoniert war, lässt sich im
Nachhinein nicht wissen, und eine Antwort zu erfinden wäre eher eine falsche
Anschuldigung als eine fehlende.

Die Impersonierung selbst wird an beiden Enden festgehalten.
`admin.user.impersonate` nennt das Konto, die Session-Zeile, die die
Impersonierung ist, und wann sie abläuft; `admin.user.impersonation_ended` nennt
dieselbe Session, wenn der Administrator sie beendet, mit dem Administrator als
Akteur. Ein Ablauf schreibt nichts, denn niemand hat gehandelt — und ebenso wenig
die Person, die sich überall abmeldet, was eine Impersonierung über
`DELETE /sessions` so beendet, wie es jede andere Session beendet. Ob es der
Person _gesagt_ wird, ist die Deployment-Einstellung `notify_impersonated_users`
([Das Deployment](deployment.md#acting-as-another-account)); ist sie aus, was die
Voreinstellung ist, ist diese Spur der einzige Datensatz.

Eine Impersonierung kann **keine externe Identität** an das Konto binden, als das
sie handelt. Einen Chat-Konto-Link zu bestätigen und den OAuth einer Integration
abzuschließen heften beide eine Identität an denjenigen, der die Anfrage ist, und
unter einer Impersonierung ist das die Zielperson — das eigene Telegram-Konto oder
der OAuth-Grant des Administrators würde sich also an das Konto einer anderen
Person heften und die Stunde überleben, auf die die Impersonierung begrenzt ist.
Beides wird während einer Impersonierung mit einem 403 abgelehnt, denn ein
Administrator, der die Verbindung eines Mitglieds repariert, ist kein Ablauf, den
diese Plattform hat. Das Mitglied verknüpft seine eigenen Konten, als es selbst.

Die Linie verläuft bei allen an das Mitglied gehefteten Zugangsdaten, nicht nur
bei einer Identität: Über das Chat-Konto und den OAuth-Grant oben hinaus wird auch
ein Bearer-Token abgelehnt, das auf die Verbindung des Mitglieds getippt wird. Ein
solches Token ist nicht die eigene Identität des Administrators, aber unter dem
Vault-Scope des Mitglieds versiegelt spricht es für jeden Agent des Mitglieds als
das Konto, zu dem es gehört, überlebt die Stunde, auf die die Impersonierung
begrenzt ist, und steht gegen das Mitglied festgehalten. Was ein Administrator
weiterhin im Namen des Mitglieds tut, ist Konfiguration, die kein Secret speichert
— ein Name, eine URL, eine Tool-Allowlist — und das Löschen eines gespeicherten
Tokens, was nichts aufbewahrt.

Die geteilte Verbindung **einer Organisation** ist dasselbe, obwohl sie von Admins
eingetragen werden soll: Ein Bearer-Token, das unter einer Impersonierung auf eine
solche getippt wird, wird ebenfalls abgelehnt, denn unter dem Vault-Scope der
Organisation versiegelt spricht es für jeden Agent, den die Organisation bindet,
als das eigene Konto des Administrators, über die Stunde hinaus, in der die
Impersonierung endet. Eine Org-Verbindung zu erstellen hielt den Administrator
dahinter bereits fest; eine zu aktualisieren hielt gar nichts fest, sodass ein auf
eine bestehende Verbindung rotiertes Token jetzt dieselbe Spur hinterlässt (#1521).

## Aufbewahrung { #retention }

Bis #1420 wurde nichts nach Zeitplan gelöscht. Gespräche, ihre Dateien,
Run-Zeilen und Manifeste, Workspaces, das Gedächtnis eines Agenten, hochgeladene
Dokumente und Audit-Einträge lebten, bis jemand die Organisation löschte. Das ist
in die eine Richtung ein Datenschutzproblem und — beim Audit — in die andere ein
Compliance-Problem: HIPAA §164.316(b)(2) will einen Audit-Eintrag sechs Jahre
aufbewahren, die DSGVO will alles andere minimieren. Die Frist gilt deshalb **pro
Klasse**, und beide Pflichten bekommen eine Einstellung.

Zu setzen unter **Organisationen → ein Workspace → Mitglieder → Aufbewahrung**,
abgesichert über `org:settings`. Ein Sweep läuft einmal täglich und löscht
**hart**: eine Richtlinie, die die Zeilen behielte, wäre keine.

Daten der [Virtual Tables](virtual-tables.md#limits-and-retention) gehören zum selben Sweep,
mit Fristen für das ganze Deployment, die keine Organisation setzt: Idempotenz-Receipts nach
24 Stunden, zugestellte Outbox-Zeilen nach 3 Tagen und die History eines Datensatzes nach
365 Tagen, wodurch auch die History eines gelöschten Datensatzes entfernt wird. Der Eintrag,
den er schreibt, nennt `table_receipts`, `table_outbox` oder `table_history` und eine Zahl.

Die drei eigenen Zahlen des Deployments - `retention_defaults`,
`retention_max_days` und `audit_retention_floor_days` - sind Felder der
Deployment-Einstellungen, von einer App-Administratorin über
`PATCH /admin/deployment-settings` geschrieben wie jede andere Einstellung dort.
Ein Konsolenformular dafür gibt es noch nicht; die Seite der Organisation ist der
Ort für die Fristen je Tenant.

### Die Klassen { #the-classes }

| Klasse | Was mitgeht | Gemessen ab |
| --- | --- | --- |
| Gespräche | Nachrichten, Tool-Aufrufe und Dateien; eine gesperrte Charge lässt Gespräche mit nicht abgeschlossenen Runs aus, und ein dauerhafter Auftrag zur Dateilöschung wird zusammen mit den Zeilen bestätigt | Der letzten Aktivität des Threads, damit einer, zu dem jemand zurückkehrt, nicht alt ist |
| Runs | Die Run-Zeile, ihr Manifest und ihre Tool-Freigaben | Dem Start des Runs |
| Workspaces | Die Aufzeichnung der Plattform über die Dateien eines Agenten. Beim `state`-Backend _ist_ die Zeile der Speicher; die Dateien eines Sandbox-Backends räumt dessen eigene TTL ab. Die eines Projekts bleiben, solange das Projekt besteht | Der letzten Nutzung |
| Apps | Eine [veröffentlichte Seite](artifacts.md), alle Versionen, Grants und der öffentliche Link; die Dateilöschung wird in derselben Transaktion eingereiht | Der letzten Veröffentlichung, damit ein Bericht, den ein Zeitplan neu veröffentlicht, nicht alt ist |
| Gedächtnis | Die Gedächtnisdateien eines Agenten | Dem letzten Schreiben, denn eine Notiz wird einmal geschrieben und monatelang gelesen |
| Hochgeladene Dokumente | Die Zeile, ihre Vektoren und die hochgeladene Datei | Dem Zeitpunkt des Hochladens |
| Audit | Einträge auf der Spur dieser Organisation | Dem Zeitpunkt des Eintrags |

**Ein Dokument, das noch eingelesen wird, ebenfalls nicht.** Ein Worker hält es,
und ihm Zeile und hochgeladenes Original unter einer Ingestion wegzunehmen, die
danach Vektoren schreibt, hinterlässt durchsuchbaren Inhalt, den kein späterer
Sweep benennen kann. Ausgemustert werden nur fertige und fehlgeschlagene Zeilen.

**Ein von einem Konnektor synchronisiertes Dokument wird nicht weggeräumt.**
Seine Lebensdauer gehört der Quelle, die es dort abgelegt hat: es hier zu löschen
entfernte eine Zeile, die der nächste `new_only`-Sync aus derselben unveränderten
Datei wieder anlegt — Embedding-Kosten für nichts. Weggeräumt wird, was jemand
hochgeladen hat und dessen Lebensdauer nichts anderes besitzt.

### Welche Zahl gewinnt { #which-number-wins }

Drei Schichten, aufgelöst in `app/core/retention.py` und sonst nirgends:

1. **Die Vorgabe des Deployments**, für eine Organisation, die nichts gesagt hat.
   Fehlt sie, heißt das für immer — eine Plattform, die beim Upgrade begänne, die
   Historie einer bestehenden Installation zu löschen, wäre eine, der beim
   nächsten Upgrade niemand mehr traute.
2. **Die eigene Frist der Organisation**, kürzer oder länger.
3. **Die Obergrenze des Deployments**: nichts dieser Klasse lebt hier länger als
   N Tage, und eine Organisation kann sie nicht anheben.

Beim Audit läuft es andersherum. Das Deployment setzt eine **Untergrenze** — wie
kurz ein Eintrag höchstens leben darf, sechs Jahre, bis eine Betreiberin das
ändert — und eine Organisation darf sie verlängern, nie verkürzen. **Audit wird
noch nicht weggeräumt**: die Frist wird aufgelöst und gemeldet und eine
Organisation an die Untergrenze gehalten, aber kein Eintrag gelöscht - die
Hash-Kette und ihr Append-only-Checkpoint stehen darauf, dass Einträge nirgendwo
hingehen, und ein bloßes Löschen lässt `audit-verify` die Ausmusterung als
Manipulation melden. Eine Kette auszumustern und die Unversehrtheit der übrigen
weiterhin beweisen zu können, ist ein Mechanismus, den es hier noch nicht gibt -
also gilt die Untergrenze, und der Sweep lässt das Audit in Ruhe.

Eine Obergrenze gilt für das Audit weiterhin, wo die beiden einander nicht
widersprechen. Wo doch, gewinnt die Untergrenze, und der Widerspruch wird
gemeldet. Eine Spur, die
eine Administratorin kürzen kann, ist keine Spur, also wird eine Frist unterhalb
der Untergrenze **abgelehnt** statt still auf sie angehoben: Einträge länger zu
behalten als die Zahl auf dem Bildschirm sagt, ist ein Fehler eigener Art.

Eine Untergrenze über einer Obergrenze ist ein Widerspruch, und er wird gemeldet
statt aufgelöst. Die Einstellungsseite nennt die Klasse; eine Administratorin
entscheidet, welche gilt. Eine der beiden zu wählen ließe ein Deployment
zurück, das sich anders verhält als seine eigene Einstellungsseite.

### Was eine Löschung überlebt { #what-survives-a-purge }

**Die Rechnung.** Die Ausgaben eines Monats sind eine Summe über `agent_runs`;
diese hart zu löschen würde den Monatswert einer Organisation mit dem
vorbeiziehenden Fenster auf null fallen lassen, und eine auf dieser Zahl
gemessene Obergrenze griffe für den Rest des Monats nicht mehr. Der Sweep liest,
was die ablaufenden Runs gekostet haben, bevor er sie löscht, und hält eine Summe
je Organisation und Monat in `purged_run_spend` — eine Zahl und einen Zähler,
ohne Agent, ohne Modell, ohne jemandes Namen. `app/services/spend.py` addiert sie
zur laufenden Summe, und das ist die einzige Stelle, an der beide sich treffen.

**Die Spur des Sweeps selbst.** Ein Eintrag je Organisation und Sweep, der die
Klasse und den Zähler nennt und sonst nichts: ein Audit-Eintrag, der zitierte, was
er gelöscht hat, hielte den Inhalt über die Aufbewahrung hinaus, die ihn entfernt
hat.

### Wenn es fehlschlägt { #when-it-fails }

Pro Klasse, nicht pro Sweep. Ein ausgefallener Vector Store darf nicht
verhindern, dass Gespräche gelöscht werden; also wird jede Klasse versucht, ihr
Fehler protokolliert und im Audit-Eintrag benannt, und der Sweep geht weiter. Der
nächste Durchlauf versucht es erneut, denn eine Charge, die nichts entfernt hat,
kommt einfach wieder vorbei.

Gelöscht wird in Chargen von 500, bis zu vierzig Durchläufe je Klasse und Sweep.
Eine Organisation, die nach zwei Jahren auf neunzig Tage stellt, arbeitet ihren
Rückstand über mehrere Tage ab statt in einem Sweep, der eine Stunde lang eine
Sperre hält, mit allen anderen periodischen Flows dahinter in der Schlange.

Jede Klasse läuft in einem Savepoint: Ein SQL-Fehler nimmt ihre Änderungen und
Löschaufträge zurück, ohne spätere Klassen abzubrechen. Dauerhafte Jobs entfernen
Dateien erst nach dem Commit. Speicherfehler und Wiederholungen sind unter
Background jobs sichtbar. Ein Rollback der Löschung lässt die Dateien intakt.

### Was das nicht erreicht { #what-this-does-not-reach }

- **Backups.** Eine Löschung entfernt Zeilen und Dateien aus dem laufenden
  Deployment. Was Ihr Backup-Zeitplan hält, lassen Sie selbst ablaufen, und ein
  Restore bringt zurück, was der Snapshot enthielt.
- **Was Sie anderswohin geschickt haben.** Logs an einen externen Collector,
  Traces in einem gehosteten Observability-Projekt und alles, was ein
  Modellanbieter behält, richten sich nach den Einstellungen jener Dienste, nicht
  nach dieser.
- **Steuerung je Gedächtniseintrag**, das ist [#1594](https://github.com/vstorm-co/agenticos/issues/1594),
  und das Löschen einer Person quer durch eine Organisation, das ist
  [#1421](https://github.com/vstorm-co/agenticos/issues/1421). Diese Seite handelt
  vom Alter; jene handeln von einer Person.

## Was all das nicht abdeckt { #what-none-of-this-covers }

Es ist wert, gesagt zu werden, weil eine Governance-Seite anderes nahelegt:

- **Keine Ratenbegrenzung je Agent.** Die vorhandenen Limits sitzen auf den
  öffentlichen Oberflächen — das Embed-Widget misst Nachrichten je Besucher, ein
  Channel-Bot misst jeden Absender ([Channels](channels.md)) — nicht auf dem
  Deployment: Es gibt kein Anfragebudget je Agent, und die eigenen Routen der
  Konsole werden nicht gemessen.
- **Keine Inhaltsfilterung.** Was ein Agent sagt, ist das, was das Modell gesagt
  hat.
- **Keine Egress-Kontrolle bei MCP.** Ein gebundener Server wird vom Worker über
  das Netzwerk erreicht; einzuschränken, wohin das gehen darf, ist
  Deployment-Konfiguration und keine Einstellung hier.

## Zusammenfassung { #recap }

- **Zwei Caps, und sie lassen sich nicht zusammenlegen.** Ein Budget begrenzt
  Geld; ein Step-Limit begrenzt eine Schleife, die je Aufruf billig ist und nie
  fertig wird.
- Das Budget wird **vor** jeder Modellanfrage geprüft, und ein fehlgeschlagener
  Run hält trotzdem fest, was er ausgegeben hat.
- Das Cap ist eine Obergrenze auf **committete** Ausgaben. Gleichzeitige Runs
  können einander nicht sehen, ein striktes Cap bedeutet also eine Queue.
- Eine **Freigabe wird einmal entschieden**, und eine zweite Entscheidung über
  eine entschiedene Freigabe wird abgelehnt.
- Einen Run zu lesen ist **autorisiert, nicht besessen** — `runs:view` liest den
  Run eines Kollegen, und der eines anderen Tenants liest sich als abwesend.
- **Stille bedeutet etwas.** Ein Alert, der nicht ankam, heißt, dass die Sache
  nicht passiert ist, und das stimmt nur, weil hier nichts nach bestem Bemühen
  läuft.
- **Eine Promotion an ihren Evals vorbei ist ein Übergehen**, mit Begründung,
  eigener Berechtigung und einem Eintrag an der Version.

## Referenz { #reference }

- [Konzepte](concepts.md) - Spec, Version, Exposure, Run.
- [Berechtigungen](permissions.md) - wer das alles setzen darf.
- [Konfiguration](configuration.md) - die Einstellungen auf Deployment-Ebene.

## Automatische Pause bei wiederholten Fehlern { #routine-health }

Routinen und Beobachtungen pausieren standardmäßig nach **drei aufeinanderfolgenden erfolglosen Prüfungen**. Im Formular lässt sich ein Limit von 1 bis 20 festlegen oder dieses Limit deaktivieren. Anbieter- und Ausführungsfehler, ungültige Beobachtungsergebnisse und unerreichbare Quellen zählen mit. Warten auf Genehmigung, einen Offline-Computer und Zustellfehler zählen nicht; eine leere, aber verifizierte erfolgreiche Prüfung setzt die Serie zurück. Andere Berechtigungs- und Budgetstopps gelten auch bei deaktiviertem Fehlerlimit.

Jede Aktivierung erhält genau eine dauerhafte Zustandsbeobachtung. Wiederholungen erhöhen den Zähler nicht; verspätete Abschlüsse werden in Aktivierungsreihenfolge ausgewertet. Die automatische Pause stoppt künftige Aktivierungen; eine laufende Modellanfrage kann noch enden.

Der bestehende Vorfall des Eigentümers vermerkt die Pause, seine Benachrichtigung verwendet gemeinsame Einstellungen und gewählte Ziele. Ein unbestätigter Vorfall erhält eine Pausenmeldung, die den früheren Inbox-Eintrag ersetzt. Fortsetzen oder eine Richtlinienänderung macht wartende Pausenmeldungen ungültig. Die Bestätigung des Vorfalls setzt weder den Ausführungszustand zurück noch die Routine fort. Nach der Fehlerbehebung startet ausdrückliches Fortsetzen ein neues Fehlerfenster. Eine Änderung des Limits startet ebenfalls ein neues Fenster.

Die Liste zeigt automatische Pause und Fehlerzahl. Höchstens 50 Beobachtungen pro Routine bleiben gespeichert, ohne Quelltexte oder Anbieterfehler. Sie gehören zum größenbegrenzten persönlichen Datenexport und werden mit Eigentümer oder Routine kaskadierend gelöscht. Migration `0275_routine_health.py` aktiviert den Standard auch für bestehende Routinen: API und Worker gemeinsam aktualisieren. Ein Downgrade entfernt Metadaten und Richtlinie, lässt die Routine jedoch pausiert.

## Geprüfte Routinequellen { #routine-sources }

Beim Erstellen hält eine Routine ihre veröffentlichte Quellenkonfiguration, konkreten MCP-Konten, Zustimmungsgenerationen und bekannten Werkzeugnamen fest. Die Prüfung umfasst fixierte Delegaten, Wissenssammlungen, Skills, Kontext und Fähigkeitenkonfiguration; entfernte A2A-Registrierungen behalten außerdem ihre Endpunkt-, Karten- und Zugangsdatenbindung. Reine Änderungen an Anweisungen oder Modelleinstellungen können einer Veröffentlichung folgen. Neue Quellen, geänderte Konten und erneuerte Kontozustimmungen erfordern eine Prüfung im Quellenbereich des Zustimmungsdialogs durch den Eigentümer. Diese Prüfung hebt keine Aktionsfreigaben auf und gewährt keinen Ressourcenzugriff. Aktuelle Berechtigungen gelten weiterhin.

Eingereihte Aktivierungen, auf Computer wartende Arbeit und geparkte Läufe behalten ihre ursprüngliche Prüfungsidentität. Eine spätere Prüfung erweitert alte Arbeit nicht. Der Postfach-Poll prüft das Eingangskonto vor dem Lesen; Ereignisse behalten die davor gültige Zustimmung. MCP-Aufrufe verwenden geprüfte Aliase und schließen ungeprüfte Werkzeuge aus. Nicht verfügbare geprüfte Quellen führen zu einer ausdrücklichen Ablehnung. Eine vor der Ausführung erforderliche Prüfung erscheint im bestehenden Aufmerksamkeitsfall des Eigentümers und zählt nicht als Modellfehler.

Migration `0276_routine_sources.py` erteilt bestehenden Routinen keine Zustimmung. Alte Worker abarbeiten lassen, API und Worker gemeinsam aktualisieren und alte Routinen vor dem Ausführen ausdrücklich prüfen. Nur der Eigentümer in der Konsole kann die Zustimmung ersetzen; API-Schlüssel, Identitätsübernahmen und unbeaufsichtigte Agenten dürfen dies nicht.

Routinen mit Budget verwenden einen gemeinsamen Nebenläufigkeitsschlüssel der Warteschlange, damit gleichzeitige Ereignisse nicht mehrere Modellaufrufe gegen denselben abgeschlossenen Ausgabenstand zulassen. Speicherextraktion läuft ebenfalls nacheinander pro Wartungsrolle einer Organisation. Jeder Auftrag prüft das aktuelle Budget erneut; ein bereits zugelassener Aufruf darf noch über die Grenze hinaus enden.

Bereitstellungsweite Meldungen über pausierte Jobs behalten den normalen Gelesen-/Verworfen-Status einer Benachrichtigung; sie werden nicht als Organisationsfälle dargestellt.

### Routinen, Arbeitsgespräche und Inbox { #routines-working-conversations-and-inbox }

**Routines** listet zeitgesteuerte Aufgaben, ereignisgesteuerte Aufgaben und
Vorlagen zur lesenden Überwachung gemeinsam. Ein Trigger bestimmt, _wann_ eine
Routine läuft; die Überwachungsvorlage begrenzt, _was_ sie tun darf. Für eine
Verantwortung genügt eine Routine, statt Watch und Zeitplan für dasselbe Postfach.

Wählen Sie **Nach jeder abgeschlossenen Ausführung** (`result_mode: report`, der
bisherige Standard) für Berichte oder **Nur wenn etwas Aufmerksamkeit braucht**
(`result_mode: inbox`) für stille Prüfungen. Der Agent liefert relevante
Quellelemente mit stabilen Element- und Revisionskennungen, Kontext und dem
vollständigen angeforderten Entwurf. Ein leeres Ergebnis erzeugt weder Gespräch
noch Abschlussmeldung. Jedes Element erhält ein privates Arbeitsgespräch und
einen Inbox-Vorgang. Wiederholte Revisionen werden dauerhaft dedupliziert, auch
nach Erledigung. Eine neue Revision aktualisiert dasselbe Gespräch und öffnet den
Vorgang erneut; stummgeschaltete Elemente bleiben stumm.

Öffnen Sie das Gespräch aus der Inbox, um den Entwurf zu ändern oder den nächsten
Schritt zu beauftragen. Lesen und Bearbeiten versenden nichts und erledigen den
Vorgang nicht. Werkzeugberechtigungen und Freigaben gelten weiterhin. Über die
Plattform-MCP-Werkzeuge `list_inbox_cases` und `settle_inbox_case` findet der Agent
den Vorgang und erledigt ihn nach erfolgreicher beauftragter Arbeit oder einer
ausdrücklichen Erledigungsbestätigung. Inbox und Chat teilen den Zustand; beim
Abschluss entfallen ausstehende Benachrichtigungen. Lesende Überwachung kann
Entwurfstext im Gespräch vorbereiten, aber nicht im Quelldienst speichern oder senden.

Nächtliche Vorschläge bleiben eine optionale, unabhängige Analyse jüngster Anfragen
und Erinnerungen, kein weiterer Postfachscan. Überwachung nutzt das Modell ihres
gewählten Agenten und benötigt kein Vorschlagsmodell. Die Organisation bestimmt
weiterhin Verfügbarkeit, Häufigkeit und das gemeinsame Hintergrundbudget pro
Person. Gewöhnliche Routinen behalten Agenten-, Organisations- und optionale eigene
Budgetgrenzen. Tage und Uhrzeit bestimmen auch die Zusammenfassung nicht dringender Funde.

Die morgendliche Zusammenfassung listet bereits gespeicherte, nicht dringende
Funde ohne weiteren Modellaufruf oder Quellenscan. Sie ist eine Benachrichtigung,
keine zweite Routine. Die Inbox-MCP-Werkzeuge stehen angemeldeten Sitzungen und
vertrauenswürdigen Zugangsdaten eines privaten Chats zur Verfügung; Letztere
sind auf Vorgänge dieses Chats beschränkt. Gewöhnliche API-Schlüssel,
Hintergrundläufe und Ersatzidentitäten des Herausgebers dürfen private
Inbox-Vorgänge weder lesen noch erledigen.