# React-Hooks

Führe eine Beeuni-Sitzung in React mit dem useSession-Hook aus.

Canonical page: https://beeuni.ai/docs/de/sdk/react

`@beeuni/sdk/react` ergänzt das SDK um React-Hooks. Diese Seite behandelt
`useSession`, den Hook, der eine Sitzung von Anfang bis Ende ausführt, sowie die anderen Hooks,
die für React-Apps als stabil bestätigt sind.

## useSession(projectId, sessionId, options?)

`useSession` startet die Sitzung, öffnet den vom Server ausgewählten Ereignistransport und
synchronisiert Nachrichten, Status und ausstehende Prompts. Rufe ihn einmal pro Sitzungsansicht auf.

```tsx
import { useSession } from '@beeuni/sdk/react';

function Chat({ projectId, sessionId }: { projectId: string; sessionId: string }) {
  const s = useSession(projectId, sessionId);

  if (s.phase !== 'ready') return <Booting stage={s.stage} onRetry={s.retry} />;

  return (
    <>
      {s.messages.map(({ info, parts }) => (
        <Message key={info.id} info={info} parts={parts} />
      ))}
      <Composer busy={s.isBusy} onSend={s.send} onStop={s.cancel} />
    </>
  );
}
```

Für die Bereitschaft ist der Server maßgeblich. Die Laufzeit ist bereit, wenn `POST /start`
`stage: 'ready'` zurückgibt. `useSession` führt keine separate clientseitige Zustandsprüfung durch.

<a id="returns"></a>

### Rückgabewerte

| Feld | Typ | Inhalt |
|---|---|---|
| `phase` | `'starting' \| 'ready' \| 'error'` | Gesamtzustand. Zeige einen Startbildschirm bis `ready`. |
| `messages` | `{ info, parts }[]` | Die Nachrichtenliste. Teile werden live gestreamt. |
| `savedTranscript` | `'loading' \| 'shown' \| 'none'` | Ob die gespeicherte Unterhaltung angezeigt werden kann, bevor der Computer aufwacht. `loading`: Eine gespeicherte Kopie ist unterwegs. `shown`: Nachrichten sind in der Liste. `none`: Nichts kann angezeigt werden, bis die Laufzeit antwortet. |
| `conversationEmpty` | `boolean` | Die gespeicherte Kopie belegt, dass die Unterhaltung leer ist, seitdem kein Turn beendet wurde und nichts offen ist oder in der Warteschlange steht. Zeige das Eingabefeld statt eines Startbildschirms. |
| `status` | `SessionStatus` | Der Sitzungsstatus. |
| `isBusy` | `boolean` | Der Agent generiert eine Antwort. |
| `questions`, `permissions` | array | Ausstehende Agentenfragen und Anfragen zur Tool-Freigabe. Ein `permission` ist hier eine einzelne Tool-Freigabe der Laufzeit, keine IAM-Berechtigung. |
| `diffs`, `todos` | array | Live-Dateidiffs und To-do-Einträge. |
| `sendError` | `BeeuniSendError \| null` | Der letzte `send`-Fehler: `billing`, `runtime-not-ready` oder `runtime-error`. |
| `rewindMessageId` | `string \| null` | Die ausgewählte Benutzernachricht, während ein umkehrbares Zurücksetzen vorbereitet ist. |
| `rewindPending` | `boolean` | Eine Anfrage zum Zurücksetzen oder Wiederherstellen läuft. |
| `rewindError` | `BeeuniSendError \| null` | Der letzte Fehler beim Zurücksetzen oder Wiederherstellen. |
| `models`, `agents`, `defaultAgent`, `commands` | — | Auswählbare Modelle, auswählbare Agenten, der Standardagent und Slash-Befehle. Verfügbar, bevor die Laufzeit startet. |
| `retry` | `() => void` | Erzwingt eine erneute Prüfung von `/start`. |

<a id="actions"></a>

### Aktionen

| Aktion | Funktion |
|---|---|
| `send(text, override?)` | Sendet einen Prompt. `override` setzt `{ model?, agent? }` nur für diese Nachricht. |
| `sendParts(parts, override?)` | Sendet Text und Laufzeit-URL-Dateiteile über den ausgewählten Transport. Private vorbereitete Handles verwenden die Prompt-Routen der Plattform. |
| `rewind(messageId)` | Setzt diese kanonische Sitzung auf eine Benutzernachricht zurück. Die ausgewählte Nachricht und der spätere Verlauf werden ausgeblendet und bleiben wiederherstellbar. |
| `restoreRewind()` | Stellt den entfernten Verlauf wieder her, bevor ein weiterer Prompt seinen Ersatz festschreibt. |
| `cancel()` | Stoppt den aktuellen Lauf und entfernt ausstehende Fragen und Berechtigungen. Hält zuerst die Prompts in der Warteschlange der Sitzung zurück, damit ein wartender Prompt keinen neuen Lauf startet. |
| `runCommand(command, args)` | Führt einen Slash-Befehl des Projekts aus. |
| `answerQuestion(id, answers)` | Beantwortet eine ausstehende Agentenfrage. |
| `rejectQuestion(id)` | Lehnt eine ausstehende Agentenfrage ab. |
| `answerPermission(id, reply, message?)` | Beantwortet eine Anfrage zur Tool-Freigabe. `reply` ist `'once'`, `'always'` oder `'reject'`. |

`useSession` gibt auch `removeQuestion` und `removePermission` zurück. Verwende sie
nicht. Sie entfernen den Prompt aus dem lokalen Zustand, benachrichtigen aber nie den Agenten, sodass
der Lauf blockiert bleibt. Verwende stattdessen `answerQuestion`, `rejectQuestion` oder
`answerPermission`.

<a id="options"></a>

### Optionen

| Option | Standard | Funktion |
|---|---|---|
| `waitMs` | `15000` | Das an `/start` gesendete Long-Poll-Budget. |
| `replayStartStash` | `true` | Sendet einen Prompt erneut, der vor dem Anlegen der Sitzung gespeichert wurde, sobald die Sitzung bereit ist. |
| `enabled` | `true` | Setze `false`, um den Hook zu verzögern, etwa bis eine Abrechnungsprüfung erfolgreich ist. |
| `chatEngine` | `true` | Setze `false`, wenn deine App eine eigene Chat-Oberfläche für diese Sitzung einbindet, um Nachrichten nicht doppelt zu synchronisieren. |
| `initialRuntimeSessionId` | `null` | Ein Laufzeitsitzungs-Pin, den dein Server bereits für diese Sitzung hält. Das Transkript wird daraus angezeigt, während `/start` läuft; die Antwort von `/start` bleibt maßgeblich. `initialOpenCodeSessionId` ist der veraltete Name dafür. |
| `browserPresence` | `false` | Hält die Präsenz-Lease dieses Tabs, während eine angemeldete Person die Seite nutzt (Eingabe in den letzten 10 Minuten). Die Lease hält den Computer der Sitzung wach. |
| `presenceAlerts` | `false` | Dieser Tab zeigt eigene Benachrichtigungen an. Wird über die Präsenz-Lease gesendet: Der Server überspringt Telefon und Web Push nur, während ein benachrichtigender Tab genutzt wird. Eine Änderung wird sofort gesendet. Übergib `true` nur, wenn das Flag `notification_center` des Projekts aktiviert ist; andernfalls ignoriert der Server es. Benötigt `browserPresence`. |
| `presencePageExit` | `false` | Beendet die Präsenz-Lease, wenn die Seite geschlossen wird oder in den Zurück-/Vorwärts-Cache wechselt, damit ein Turn, der direkt nach dem Schließen des Tabs endet, weiterhin eine Benachrichtigung auslöst. Aktiviert: `pagehide` meldet Abwesenheit, und jede Abwesenheitsmeldung wird mit `keepalive` gesendet, sodass sie die Seite überdauert. Deaktiviert: keine `pagehide`-Meldung und kein `keepalive`. Ein sich schließender Tab wird weiterhin ausgeblendet und meldet Abwesenheit, aber der Browser kann diese Anfrage beim Entladen der Seite abbrechen; die Lease bleibt dann bis zu ihrem Ablauf nach 90 s bestehen. Übergib das Flag `notification_center` des Projekts. Eine Änderung gilt sofort, ohne neue Präsenzmeldung. Benötigt `browserPresence`. |
| `repositoryMode` | — | Nur Telemetrie. Eine Sitzung, die vor einem Repository-Austausch erstellt wurde, startet und läuft auch ohne diese Option mit der aktuellen Projektkonfiguration. Nur ihr `/workspace`-Klon stammt noch aus dem alten Repository, daher erfordert ein Push daraus zuerst einen Rebase. |

Das Senden erfolgt optimistisch. `send` zeigt deine Nachricht sofort an; anschließend ergänzen Stream-
Ereignisse die Antwort des Agenten.

`rewind(messageId)` erstellt niemals eine Sitzung. Es verwendet die kanonische Sitzung aus
`POST /start`. Die Laufzeit stellt den Dateizustand wieder her und hält den entfernten Transkriptverlauf
wiederherstellbar. Der nächste akzeptierte Prompt schreibt den Ersatzverlauf fest.

<a id="other-stable-hooks"></a>

## Weitere stabile Hooks

`@beeuni/sdk/react` exportiert außerdem React Query-Hooks für Daten, die keine
laufende Sitzung benötigen. Jeder entspricht einer Methode des
[Clients](/docs/de/sdk/reference) und benötigt keinen Provider.

| Hook | Liest |
|---|---|
| `useProjectModels(projectId)` | Auswählbare Modelle für das Projekt. |
| `useProjectModelPickerCatalog(projectId)` | Den rohen `/model-picker`-Datensatz pro Wire-Modell (`reasoning_options`, `temperature`, `limit`) und `managedPricingRoutes`-Kundenpreisschätzungen für jede geeignete verwaltete Route. Die bedienende Route bestimmt die endgültige Credit-Abbuchung. |
| `useProjectSessions(projectId, { parent, startedBy, q, limit })` | Die Sitzungen eines Projekts, seitenweise. `parent: 'root'` gibt Sitzungen der obersten Ebene mit `child_count` zurück; `startedBy: 'me' \| 'others' \| 'automated'` filtert danach, wer den Lauf gestartet hat; `q` durchsucht jede Sitzung, die der Betrachter öffnen darf, nicht nur geladene Seiten. Jeder Filtersatz hat seinen eigenen Cache-Eintrag. |
| `useSessionChildren(projectId, parentSessionId, { q, limit, enabled })` | Die Sitzungen, die eine übergeordnete Sitzung erzeugt hat (`parent=<id>`), neueste zuerst, seitenweise. Setze `enabled: false`, bis die übergeordnete Sitzung geöffnet wird. |
| `useVisibleAgents({ projectId })` | Die sichtbaren Agenten des Projekts. |
| `useProjectConfig(projectId)` | Die Laufzeitkonfiguration des Projekts: Standardagent, Befehle. |
| `useProjectSecrets(projectId)` | Secrets: auflisten, hinzufügen, entfernen und persönliche Überschreibungen. |
| `useAccountSecretResources(accountId)` | Metadaten zu Anbieter-Secrets des Kontos und Nutzungsfreigaben. Werte werden nie zurückgegeben. |
| `useSessionProviderSecretPools(projectId, sessionId)` | Konfigurierte Pools, einschließlich leerer Auswahlen; `setPool` aktualisiert einen Anbieter und frischt den Sitzungscache auf. |
| `useProjectTriggers(projectId)` | Trigger: auflisten, erstellen, aktualisieren, entfernen, auslösen. |
| `useProjectTriggerEventTypes(projectId, connector)` | Die App-Ereignisse, auf die ein Connector reagieren kann. Inaktiv, bis `connector` gesetzt ist. |
| `useChangeRequests(projectId, status?)` | Änderungsanfragen: auflisten, öffnen, zusammenführen, schließen, Änderungen anfordern. |
| `useRuntimeSupports(capability)` | Ob die Laufzeit der geöffneten Sitzung eine Funktion bereitstellt (`session.rewind`, `session.compact`, …), anhand der `/beeuni/health`-Antworten, die der Poller für die Laufzeit-Wiederverbindung liest. `true` bis zur ersten Antwort. Siehe [Was die Laufzeit unterstützt](/docs/de/sdk/sessions#what-the-runtime-supports). |
| `useNotificationInbox({ userId, limit?, enabled? })` | Die neueste Posteingangsseite des Aufrufers (`limit` standardmäßig 20), `unreadCount` sowie `markRead(ids)`, `markAllRead()`, `markSessionRead(sessionId)`. Fragt alle 60 s ab, solange die Seite sichtbar ist, und ruft bei Fokus erneut ab. Ein Schreibvorgang ändert den Cache sofort, stellt ihn bei einem Fehler wieder her und ruft dann jeden Posteingangseintrag erneut ab. Keine Anfrage ohne `userId`. |
| `useNotificationPreferences({ userId, enabled? })` | Die Push- und E-Mail-Auswahl des Aufrufers pro Art, `email_available` und `update(patch)` (optimistisch). |
| `useSessionWatch({ userId, projectId, sessionId, enabled? })` | `{ watching }` für eine Sitzung und `setWatching(watching)` (optimistisch), um sie stummzuschalten oder zu verfolgen. |
| `resetRuntimeQueries(queryClient)` | Kein Hook. Entfernt jede zwischengespeicherte Laufzeitabfrage (Sitzungen, Nachrichten, Agenten, Befehle, Diffs). Rufe ihn auf, nachdem die Laufzeit der Sitzung durch einen Neustart oder ein Neuladen der Konfiguration ersetzt wurde, damit der nächste Lesevorgang aus der neuen Laufzeit erfolgt. |

Die drei Benachrichtigungs-Hooks verwenden `userId` (`qk.notifications`) als Cache-Schlüssel,
sodass ein Benutzerwechsel im selben Tab nie den Posteingang des vorherigen Benutzers zeigt. Übergib
die ID des angemeldeten Benutzers.

Benachrichtigungen werden durch das projektspezifische `notification_center`
[Feature-Flag](/docs/de/feature-flags) gesteuert, das standardmäßig deaktiviert ist. Aktiviere jeden Hook nur mit
`enabled: useFeatureFlag(projectId, 'notification_center').enabled`, damit ein
Projekt mit deaktiviertem Flag keine Benachrichtigungsanfrage sendet.

<a id="connector-setup-links"></a>

## Links zur Connector-Einrichtung

Ein Agent kann einen 1-Klick-Verbindungslink erzeugen (`/connect/<token>`). Die Daten
und Laufzeitrichtlinien des Ablaufs liegen im SDK; ein Host rendert darauf seinen eigenen Dialog und
injiziert seine Adapter.

| Export | Rolle |
|---|---|
| `useConnectorSetup(token, options)` | Der gesamte Ablauf: laden, was der Link benennt, `POST /start`, die gehostete Seite des Anbieters über das injizierte `openPopup` öffnen, `POST /finalize` nach dem Zeitplan 3 s / 5 s / 5 Minuten abfragen und `phase` (`loading`, `ready`, `starting`, `opened`, `connected`, `error`), die App-Informationen, den Fehler und die Identität melden, als die das Konto autorisiert wurde. `options` enthält `backendUrl`, den `storage`-Adapter und `openPopup` des Hosts; `onOpened` wird nach jedem Öffnen eines Popups ausgelöst. |
| `useConnectorLinkInfo(token, options)` | Ein gemeinsamer, speichergestützter Cache pro Link-Token, sodass alle Karten und das Modal einen einzigen GET-Aufruf der ratenbegrenzten öffentlichen Route teilen und das App-Logo im ersten Frame anzeigen. Dieselben `backendUrl`/`storage`-Optionen. |
| `createConnectorLinkInfoCache(fetchInfo, { storage })` | Die Cache-Factory selbst (frameworkunabhängig, Speicher injiziert) für einen Host, der eine eigene Instanz möchte. |
| `resolveConnectorStart({ start, finalize })` | Die `/start`-Ergebnisregel: gehostete URL → Popup, bereits verbunden → einmal abschließen, andernfalls die Fehlermeldung. |
| `nextConnectorPollDelay(attempt, elapsedMs)` | Der begrenzte Abfragezeitplan (zuerst 3 s, danach 5 s, endet nach 5 Minuten). |

<a id="connector-data"></a>

## Connector-Daten

`useConnectorQuery(projectId, slug, action, args, { account, enabled, staleTime })`
liest die Ausgabe einer Connector-Aktion als zwischengespeicherte Abfrage. `error` ist ein
`ConnectorCallError`: Rendere eine Verbindungsschaltfläche aus `error.connectUrl` oder eine
Kontoauswahl aus `error.availableAccounts`. Verwende ihn nur für eine Aktion, die
liest; jeder Abruf führt die Aktion aus und schreibt ein Audit-Ereignis. Die Ausgabe bleibt
30 s aktuell, bei Fokus wird nie erneut abgerufen, und nur ein vorgelagertes `429` oder `503` wird
wiederholt. Siehe [Connectors aus dem Code](/docs/de/sdk/reference#pconnectorslug--one-connector).

<a id="start-the-open-read-early"></a>

## Den Lesevorgang zum Öffnen früh starten

`prefetchSessionOpen(queryClient, projectId, sessionId)` startet den Lesevorgang zum Öffnen der Sitzung,
bevor die Sitzungsansicht eingebunden wird. Er sendet eine `GET .../snapshot`-Anfrage
(Sitzungsdatensatz, Turn, Warteschlange und die ersten 40 Transkriptnachrichten) und befüllt daraus den
Cache für den Sitzungsdatensatz. `useSession` nutzt dann diese Anfrage, statt
eine eigene zu starten.

Rufe ihn auf, wenn eine Route eine Sitzung benennt, oder bei Hover, Fokus oder Berührung eines
Sitzungslinks. Er liest nur: Er ruft nie `/start` auf und weckt nie einen Computer. Er
wird nie abgelehnt. Ein erneuter Aufruf für dieselbe Sitzung innerhalb von 30 Sekunden sendet keine
Anfrage.

```tsx
import { prefetchSessionOpen } from '@beeuni/sdk/react';

<a
  href={`/projects/${projectId}/sessions/${sessionId}`}
  onFocus={() => void prefetchSessionOpen(queryClient, projectId, sessionId)}
/>;
```

<a id="render-from-cache-on-a-reload"></a>

## Bei einem Neuladen aus dem Cache rendern

Zwei Speicher bewahren auf, was ein Benutzer bereits gesehen hat, sodass es beim Neuladen oder einem Kaltstart der App
im ersten Frame gerendert und an Ort und Stelle aktualisiert wird. Beide sind benutzerspezifisch und begrenzt, und ein
Host leert beide beim Abmelden.

- `createPersistedQueryCache` speichert die React-Query-Ergebnisse, anhand derer ein Benutzer navigiert:
  Konten, Projekte, Projektdetails, die seitenweise Sitzungsliste und Sitzungsdatensätze
  (`isPersistableQueryKey`). Ein wiederhergestellter Eintrag behält sein ursprüngliches Alter und wird daher
  beim Einbinden erneut abgerufen. Die Standardgrenzen sind 1.000.000 Zeichen und 7 Tage.
- `createSavedCopyStore` speichert die zuletzt gespeicherten Kopien der Transkripte, die der Server
  gesendet hat. Registriere ihn mit `setSavedCopyStore`, und `useSession` rendert die gespeicherte Kopie
  vor dem ersten Frame. Die Serverkopie wird dann anhand der Nachrichten-ID damit abgeglichen, und
  das SDK liest sie 3 Sekunden nach dem Ende jedes Turns erneut. Die Standardgrenzen sind 12
  Sitzungen, 1.500.000 Zeichen und 14 Tage. Bei einer Kopie mit mehr als 300.000 Zeichen werden
  die neuesten Nachrichten behalten, die hineinpassen; ältere Nachrichten werden vom Server geladen, wenn der Benutzer
  nach oben scrollt.

```tsx
import {
  browserKeyValueStorage,
  createPersistedQueryCache,
  createSavedCopyStore,
  isPersistableQueryKey,
  setSavedCopyStore,
} from '@beeuni/sdk';

const storage = browserKeyValueStorage(); // null where there is no localStorage
if (storage) {
  const cache = createPersistedQueryCache({ storage, userId, shouldPersist: isPersistableQueryKey });
  cache.restore(queryClient); // synchronous with localStorage: no pending frame
  const stop = cache.persist(queryClient);
  setSavedCopyStore(createSavedCopyStore({ storage, userId }));
}
```

Übergib unter React Native AsyncStorage als `storage`. `restore` gibt dann ein Promise zurück.

Der Speicher für gespeicherte Kopien enthält nur Serveraufzeichnungen: den Transkriptspiegel, den die API
schreibt, wenn ein Turn endet. Das Live-Transkript wird nie auf das Gerät geschrieben, sodass ein
angehaltener Turn nicht als laufender zurückkehren kann. Ein Laufzeit-Lesevorgang, der keine Nachrichten zurückgibt,
belässt die gespeicherten Nachrichten auf dem Bildschirm: Das bedeutet, dass der Computer seinen Zustand verloren hat, nicht, dass die
Unterhaltung leer ist. Der erste Laufzeit-Lesevorgang mit Nachrichten gleicht sie ab.

<a id="sign-out-and-account-switching"></a>

## Abmelden und Kontowechsel

Das SDK hält benutzerspezifischen Sitzungszustand im Arbeitsspeicher: Transkripte, ausstehende Fragen
und Berechtigungen, Turn-Bestätigungen und Modellauswahlen. Rufe `resetIdentityState()` bei
jedem Identitätswechsel auf: beim Abmelden und wenn sich ein anderer Benutzer anmeldet. Dazu gehört auch
eine Anmeldung in einem anderen Tab, die den Benutzer ohne Neuladen der Seite ändert.

Rufe diesen Handler auf, wenn dein Anmeldeablauf einen Nutzer an- oder abmeldet.

```tsx
import { resetIdentityState } from '@beeuni/sdk/react';

let previousUserId: string | null = null;

function onIdentityChange(userId: string | null) {
  if (userId !== previousUserId) {
    queryClient.clear();
    resetIdentityState();
  }
  previousUserId = userId;
}
```

`resetIdentityState()` entfernt auch den Eintrag `localStorage` aus dem Modellspeicher. Es
leert weder die eigenen Caches des Hosts, etwa React Query, noch die an
`createBeeuni` übergebene Tokenquelle. Setze diese im selben Handler zurück. Es wirft nie eine Ausnahme.

Bei den Speichern aus [Beim Neuladen aus dem Cache rendern](#render-from-cache-on-a-reload)
leerst du sie im selben Handler: `await cache.clear()`, dann
`await savedCopies.clear()` und `setSavedCopyStore(null)`. Keiner der beiden Speicher stellt jemals
Daten für einen anderen Benutzer wieder her, aber ein abgemeldetes Gerät sollte sie nicht behalten.

<a id="next"></a>

## Weiter

- [Sitzungen](/docs/de/sdk/sessions) — das Sitzungshandle, das `useSession` umschließt, und die `BeeuniSendError`-Arten.
- [Referenz](/docs/de/sdk/reference) — die vollständige REST-Schnittstelle, aus der diese Hooks lesen.

<a id="saved-history-during-startup"></a>

## Gespeicherter Verlauf beim Start

`useSession(projectId, sessionId)` zeigt den gespeicherten Chatverlauf an, während sein Computer startet.
Das SDK liest die Datenbank unabhängig von der Bereitschaft der Laufzeitumgebung. Live-Nachrichten werden dann
anhand ihrer ursprünglichen IDs abgeglichen. Hosts benötigen weder eine separate Verlaufsabfrage noch einen Nachrichtenspeicher.

Der gespeicherte Verlauf wird genau wie das Live-Transkript dargestellt: Text, Reasoning und jeder Tool-Aufruf
mit Eingabe, Ausgabe und Fehler. Die vollständige Ansicht eines Sub-Agenten liest dessen eigenes gespeichertes
Transkript: Übergib den Scope des übergeordneten Agenten und `savedChild: true` an
`useSessionSync(subAgentSessionId, { beeuniSessionScope, savedChild: true })`. Das erste Fenster enthält die neuesten 40 Nachrichten mit
höchstens 1.000.000 Zeichen. Beim Hochscrollen werden ältere Fenster aus dem gespeicherten Verlauf geladen, auch wenn
der Computer ausgeschaltet ist. Anhänge, die den Computer benötigen, werden verfügbar, sobald die
Laufzeitumgebung bereit ist.

`savedTranscript` teilt einem Host mit, was er während des Computerstarts darstellen soll. Es ist `loading`, solange
noch eine gespeicherte Kopie eintreffen kann, `shown`, sobald Nachrichten in `messages` vorliegen, und `none`, wenn nichts
angezeigt werden kann, bevor die Laufzeitumgebung antwortet. Ein unbekannter Zustand ist `loading`, niemals `none`.

`conversationEmpty` ist wahr, wenn es nichts gibt, worauf gewartet werden muss. Die gespeicherte Kopie belegt, dass die
Unterhaltung leer ist: Beim vollständigen Lesen der Laufzeitumgebung der Sitzung wurden keine Nachrichten gefunden. Seitdem wurde kein Turn beendet,
und nichts ist offen oder in der Warteschlange. Fehlende Datensätze machen den Wert nie wahr, sodass eine ältere Sitzung
mit Verlauf niemals als leer angezeigt wird. Eine Sitzung erhält diesen Nachweis bei ihrem ersten Start mit aktiviertem
gespeichertem Verlauf.

```tsx
if (s.messages.length === 0) {
  // Nothing saved and nothing to wait for: the user can type now.
  if (s.conversationEmpty) return <Composer />;
  // One round trip for a saved copy; the computer can take minutes.
  return s.savedTranscript === 'none' ? <Booting stage={s.stage} /> : <MessageSkeleton />;
}
```

Bei Tool-Aufrufen, die vor dem 2026-09-28 gespeichert wurden, gingen Eingabe und Ausgabe verloren. Der Server liefert jeden davon mit
der Eingabe aus, die sich aus dem erhaltenen Titel oder den Metadaten belegen lässt: Befehl und Ausgabe eines Befehls, Pfad eines Dateiaufrufs,
Suchmuster, Beschreibung eines Sub-Agenten, URL eines Abrufs, Todo-Liste, Name eines Skills. Beim
nächsten Start der Sitzung wird der gesamte Verlauf erneut gespeichert, 1:1.
<a id="pending-prompts"></a>

### Ausstehende Prompts

`useSessionPrompts(projectId, sessionId)` sendet an den persistenten Sitzungseingang.
Der Server führt ausstehende Prompts automatisch aus, jeden als eigenen Turn.

Übergib `placement: 'transcript'`, um eine ausstehende Nachricht in der Unterhaltung anzuzeigen.
Sie wird vor jedem `composer`-Eintrag ausgeführt und beendet die aktive Antwort, nachdem der
aktuelle Tool-Aufruf abgeschlossen ist.
Übergib `placement: 'composer'`, um sie in der bearbeitbaren Liste über dem Eingabefeld zu belassen.
Sie wartet, bis die aktive Antwort abgeschlossen ist. Jede Platzierung behält die Sendereihenfolge bei.
Übergib `delivery: 'steer' | 'queue' | 'interrupt'`, um festzulegen, wie der Prompt einen
laufenden Turn erreicht. `steer` übergibt ihn beim nächsten Schritt an den Turn; der Turn wird nicht
angehalten. `queue` wartet auf das Ende des Turns. `interrupt` beendet den Turn nach dem
laufenden Tool. Wenn nur `delivery` angegeben ist, sendet das SDK die daraus abgeleitete Platzierung:
`interrupt` ist `transcript`, die anderen sind `composer`. Eine Steer-Zeile, die nicht
steuern kann, zeigt `delivery: 'queue'` an, wobei `steer_fallback` auf `unsupported`,
`not_prompter` oder `turn_ended` gesetzt ist. `interrupt(promptId)` ("Stop and send") macht aus einer
wartenden Zeile einen Transkripteintrag; eine bereits übertragene Zeile antwortet mit `409`.

Im Web-Eingabefeld sendet Enter als `steer`, während ein Turn läuft, und als Transkript-
Sendung, während die Sitzung inaktiv ist. Command+Enter (Control+Enter unter Windows/Linux)
verwendet die Platzierung im Eingabefeld. Shift+Enter fügt einen Zeilenumbruch ein.

Listenzeilen enthalten `full_text` für vollständige Vorschauen nach dem Neuladen. `text` bleibt
die begrenzte Vorschau für ältere Clients. Anhangsbytes sind von Listenantworten
ausgeschlossen. Löschen gibt den vollständigen Prompt zurück, um das Löschen verlustfrei rückgängig machen zu können.

<a id="pending-delivery-and-active-responses"></a>

### Ausstehende Zustellung und aktive Antworten

`useSessionWorking()` behält `state: 'working'` bei, während ein Prompt im persistenten
Eingang wartet. Dadurch bleibt Stop verfügbar. `pendingDelivery: true` kennzeichnet einen Prompt, der noch
auf dem Weg zur Laufzeitumgebung ist; die Web-App zeigt immer dann einen einzelnen Aktivitätsindikator an, wenn `state`
`working` ist, unabhängig vom Wert dieses Flags. Aktive Turns und neue Aktivitäten der
Laufzeitumgebung setzen es zurück. Ein Abbruch mit Zeitüberschreitung bestätigt nicht, dass die Ausführung beendet wurde.

Die Übernahme durch einen Worker prüft nur die Zulassung und lässt den Prompt weiter warten. Die Zustellung beginnt,
nachdem die Zulassung erfolgreich war. Ein bestätigter aktiver Turn entfernt die Darstellung als ausstehend,
selbst wenn der vorherige Snapshot des Eingangs diesen Prompt noch aufführt. Aktivitäten der Laufzeitumgebung
bewahren bei dieser Übergabe die Nachrichten-ID des aktiven Turns.

Das Web-Eingabefeld bezeichnet Command/Ctrl+Enter als **Queue List** und "Stop and
send" einer Zeile als **Quick Queue**. Quick Queue-Einträge werden zuerst ausgeführt. Ein wartender Quick Queue-Eintrag erscheint als getönte Blase nach
zugestellten Turns, ohne Statustext. Queue List-Einträge bleiben bearbeitbar, bis die Zustellung beginnt.
Der Server stellt verpasste Abschlussereignisse anhand exakter Nachweise aus der Laufzeitumgebung wieder her, auch
nach dem Neuladen, ohne eine nicht erreichbare Laufzeitumgebung als abgeschlossenen Turn zu behandeln.

Jeder einzelne Druck auf Enter zeigt den ausstehenden Prompt sofort an, auch wenn eine frühere
Annahme-Anfrage noch offen ist. Nur ein Zustellungsfehler fügt Text hinzu: seine Ursache,
Retry und Remove. Der Servereingang bestimmt weiterhin die Ausführungsreihenfolge.

Der Hook liest Änderungen an der Warteschlange aus dem Steuerungsstream der Sitzung
(`GET .../sessions/:id/events?channels=control`), mit einer Verbindung pro Sitzung,
unabhängig davon, wie viele Komponenten ihn einbinden. Jeder Schreibvorgang einer Eingangszeile wird als
`beeuni.control.queue`-Frame übermittelt. Die `GET .../prompts`-Abfrage läuft nur, solange dieser
Stream nicht verbunden ist.

<a id="why-a-turn-ended"></a>

### Warum ein Turn endete

Das Transkript eines unterbrochenen Turns enthält nur `MessageAbortedError`. Die
Steuerungsebene speichert, was sie darüber weiß, wie jeder Turn endete: die von der
Sandbox genannte Ursache, etwa `SandboxMemoryGuard`, und welche Turns ohne
Angabe einer Ursache abbrachen. Ein angeforderter Stopp — über die Stop-Schaltfläche, `abort()` oder einen Prompt,
der an eine beschäftigte Sitzung gesendet wurde — wird als Anforderung erfasst und niemals als
Fehler gemeldet.

`useSessionTurnOutcome(projectId, sessionId)` gibt `last_ended`,
`recent_failures` und den Lesezeitpunkt `atMs` aus dem Cache zurück, den `useSessionWorking()`
aktuell hält. Es stellt keine Anfrage. Übergib das Ergebnis an `turnEndNotice`, zusammen mit dem, was
das Transkript über den Turn aussagt:

```tsx
const outcome = useSessionTurnOutcome(projectId, sessionId);
const notice = turnEndNotice(outcome, turn.userMessage.info.id, {
  hasError: Boolean(turnError),
  isAbort: turnErrorIsAbort,
});
// notice: { kind: 'sandbox-memory', usedPct: 97, detail: 'sandbox memory at 97% …' }
//       | { kind: 'cause', name: 'SomeGuard', message: '…' }
//       | { kind: 'unexplained' }
//       | null
```

Der Host ordnet jedem `kind` seinen eigenen Text zu und parst niemals die Nachricht der
Sandbox. `null` bedeutet „zeige, was das Transkript zeigt“: Ein tatsächlicher Fehler bleibt sichtbar, ein
Abbruch wird nicht dargestellt. Ein ungeklärter Fehler wird erst gemeldet, wenn der Lesezeitpunkt
einige Sekunden nach dem Ende des Turns liegt, da die Ursache oft einen Frame
nach dem Abbruch eintrifft. `turnEndCause(outcome, messageId)` gibt die unverarbeitete erfasste
Ursache zurück oder `null`.
