---
title: "Hooks de React"
description: "Ejecuta una sesión de Beeuni en React con el hook useSession."
---

`@beeuni/sdk/react` añade hooks de React sobre el SDK. Esta página cubre
`useSession`, el hook que ejecuta una sesión de principio a fin, y los demás hooks
confirmados como estables para aplicaciones de React.

## useSession(projectId, sessionId, options?)

`useSession` inicia la sesión, abre el transporte de eventos seleccionado por el servidor y
sincroniza los mensajes, el estado y las solicitudes pendientes. Llámalo una vez por vista de sesión.

```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} />
    </>
  );
}
```

La disponibilidad la determina el servidor. El entorno de ejecución está listo cuando `POST /start` devuelve
`stage: 'ready'`. `useSession` no ejecuta una comprobación de estado independiente en el cliente.

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

### Valores devueltos

| Campo | Tipo | Qué contiene |
|---|---|---|
| `phase` | `'starting' \| 'ready' \| 'error'` | Estado general. Muestra una pantalla de inicio hasta que `ready`. |
| `messages` | `{ info, parts }[]` | La lista de mensajes. Las partes llegan en tiempo real. |
| `savedTranscript` | `'loading' \| 'shown' \| 'none'` | Indica si la conversación guardada puede mostrarse antes de que se active el ordenador. `loading`: hay una copia guardada en camino. `shown`: los mensajes están en la lista. `none`: no se puede mostrar nada hasta que responda el entorno de ejecución. |
| `conversationEmpty` | `boolean` | La copia guardada demuestra que la conversación está vacía, que no ha terminado ningún turno desde entonces y que no hay nada abierto ni en cola. Muestra el compositor, no una pantalla de inicio. |
| `status` | `SessionStatus` | El estado de la sesión. |
| `isBusy` | `boolean` | El agente está generando una respuesta. |
| `questions`, `permissions` | array | Preguntas pendientes del agente y solicitudes de aprobación de herramientas. Un `permission` aquí es una aprobación de herramienta del entorno de ejecución, no un permiso de IAM. |
| `diffs`, `todos` | array | Diferencias de archivos y tareas pendientes en tiempo real. |
| `sendError` | `BeeuniSendError \| null` | El último error de `send`: `billing`, `runtime-not-ready` o `runtime-error`. |
| `rewindMessageId` | `string \| null` | El mensaje del usuario seleccionado mientras se prepara un retroceso reversible. |
| `rewindPending` | `boolean` | Hay una solicitud de retroceso o restauración en curso. |
| `rewindError` | `BeeuniSendError \| null` | El último error de retroceso o restauración. |
| `models`, `agents`, `defaultAgent`, `commands` | — | Modelos seleccionables, agentes seleccionables, el agente predeterminado y comandos de barra. Disponibles antes de que se inicie el entorno de ejecución. |
| `retry` | `() => void` | Fuerza una nueva comprobación de `/start`. |

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

### Acciones

| Acción | Qué hace |
|---|---|
| `send(text, override?)` | Envía una solicitud. `override` establece `{ model?, agent? }` solo para este mensaje. |
| `sendParts(parts, override?)` | Envía texto y partes de archivos de URL del entorno de ejecución mediante el transporte seleccionado. Los identificadores privados preparados usan las rutas de solicitudes de la plataforma. |
| `rewind(messageId)` | Retrocede esta sesión canónica hasta un mensaje del usuario. El mensaje seleccionado y la ruta posterior quedan ocultos y se pueden recuperar. |
| `restoreRewind()` | Restaura la ruta eliminada antes de que otra solicitud confirme su reemplazo. |
| `cancel()` | Detiene la ejecución actual y borra las preguntas y los permisos pendientes. Retiene primero las solicitudes en cola de la sesión, para que una solicitud en cola no inicie una nueva ejecución. |
| `runCommand(command, args)` | Ejecuta un comando de barra del proyecto. |
| `answerQuestion(id, answers)` | Responde a una pregunta pendiente del agente. |
| `rejectQuestion(id)` | Rechaza una pregunta pendiente del agente. |
| `answerPermission(id, reply, message?)` | Responde a una solicitud de aprobación de herramienta. `reply` es `'once'`, `'always'` o `'reject'`. |

`useSession` también devuelve `removeQuestion` y `removePermission`. No los uses. Borran la solicitud del estado local, pero nunca notifican al agente, por lo que la ejecución sigue bloqueada. Usa `answerQuestion`, `rejectQuestion` o
`answerPermission` en su lugar.

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

### Opciones

| Opción | Predeterminado | Qué hace |
|---|---|---|
| `waitMs` | `15000` | El presupuesto de sondeo prolongado enviado a `/start`. |
| `replayStartStash` | `true` | Reproduce una solicitud guardada antes de que existiera la sesión, una vez que la sesión esté lista. |
| `enabled` | `true` | Establece `false` para retrasar el hook, por ejemplo hasta que se apruebe una comprobación de facturación. |
| `chatEngine` | `true` | Establece `false` si tu aplicación monta su propia interfaz de chat para esta sesión, para evitar sincronizar los mensajes dos veces. |
| `initialRuntimeSessionId` | `null` | Un pin de sesión del entorno de ejecución que tu servidor ya conserva para esta sesión. La transcripción se muestra a partir de él mientras se ejecuta `/start`; la respuesta de `/start` sigue siendo la autoridad. `initialOpenCodeSessionId` es su nombre obsoleto. |
| `browserPresence` | `false` | Mantiene el arrendamiento de presencia de esta pestaña mientras una persona con sesión iniciada usa la página (entrada en los últimos 10 minutos). El arrendamiento mantiene activo el ordenador de la sesión. |
| `presenceAlerts` | `false` | Esta pestaña muestra sus propias notificaciones. Se envía mediante el arrendamiento de presencia: el servidor omite el teléfono y Web Push solo mientras se usa una pestaña con alertas. Un cambio se envía de inmediato. Pasa `true` solo cuando la marca `notification_center` del proyecto está activada; el servidor la ignora en caso contrario. Requiere `browserPresence`. |
| `presencePageExit` | `false` | Finaliza el arrendamiento de presencia cuando la página se cierra o entra en la caché de navegación hacia atrás/adelante, para que un turno que termine justo después de cerrar la pestaña siga enviando una notificación. Activado, `pagehide` informa de ausencia, y cada informe de ausencia se envía con `keepalive`, por lo que sobrevive a la página. Desactivado: no hay informe de `pagehide` ni `keepalive`. Una pestaña que se cierra sigue pasando a ocultarse e informa de ausencia, pero el navegador puede cancelar esa solicitud mientras descarga la página; el arrendamiento permanece entonces hasta su expiración de 90 s. Pasa la marca `notification_center` del proyecto. El cambio se aplica de inmediato, sin un nuevo informe de presencia. Requiere `browserPresence`. |
| `repositoryMode` | — | Solo telemetría. Una sesión creada antes de que comience un reemplazo del repositorio se inicia y ejecuta con la configuración actual del proyecto sin ese reemplazo. Solo su clon `/workspace` sigue procediendo del repositorio antiguo, por lo que un push desde él requiere primero un rebase. |

El envío es optimista. `send` muestra tu mensaje de inmediato y, después, los eventos de transmisión completan la respuesta del agente.

`rewind(messageId)` nunca crea una sesión. Usa la sesión canónica de `POST /start`. El entorno de ejecución restaura el estado de los archivos y mantiene recuperable la ruta de transcripción eliminada. La siguiente solicitud aceptada confirma la ruta de reemplazo.

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

## Otros hooks estables

`@beeuni/sdk/react` también exporta hooks de React Query para datos que no necesitan una sesión en ejecución. Cada uno refleja un método del [cliente](/docs/es/sdk/reference) y no necesita ningún proveedor.

| Hook | Lee |
|---|---|
| `useProjectModels(projectId)` | Modelos seleccionables para el proyecto. |
| `useProjectModelPickerCatalog(projectId)` | El registro `/model-picker` sin procesar por modelo de protocolo (`reasoning_options`, `temperature`, `limit`) y las estimaciones de precios para el cliente de `managedPricingRoutes` para cada ruta gestionada apta. La ruta que atiende la solicitud determina el cargo final en créditos. |
| `useProjectSessions(projectId, { parent, startedBy, q, limit })` | Las sesiones de un proyecto, paginadas. `parent: 'root'` devuelve las sesiones de nivel superior con `child_count`; `startedBy: 'me' \| 'others' \| 'automated'` filtra por quién inició la ejecución; `q` busca en todas las sesiones que el usuario puede abrir, no solo en las páginas cargadas. Cada conjunto de filtros tiene su propia entrada de caché. |
| `useSessionChildren(projectId, parentSessionId, { q, limit, enabled })` | Las sesiones que generó una sesión principal (`parent=<id>`), de más reciente a más antigua, paginadas. Establece `enabled: false` hasta que se abra la sesión principal. |
| `useVisibleAgents({ projectId })` | Los agentes visibles del proyecto. |
| `useProjectConfig(projectId)` | La configuración del entorno de ejecución del proyecto: agente predeterminado, comandos. |
| `useProjectSecrets(projectId)` | Secretos: listar, añadir, eliminar y anulaciones personales. |
| `useAccountSecretResources(accountId)` | Metadatos de secretos de proveedores de la cuenta y permisos de uso. Los valores nunca se devuelven. |
| `useSessionProviderSecretPools(projectId, sessionId)` | Grupos configurados, incluidas las selecciones vacías; `setPool` actualiza un proveedor y refresca la caché de sesiones. |
| `useProjectTriggers(projectId)` | Disparadores: listar, crear, actualizar, eliminar, activar. |
| `useProjectTriggerEventTypes(projectId, connector)` | Los eventos de la aplicación que pueden activar un conector. Inactivo hasta que se establezca `connector`. |
| `useChangeRequests(projectId, status?)` | Solicitudes de cambio: listar, abrir, fusionar, cerrar, solicitar cambios. |
| `useRuntimeSupports(capability)` | Si el entorno de ejecución de la sesión abierta ofrece una función (`session.rewind`, `session.compact`, …), según las respuestas de `/beeuni/health` que lee el sondeador de reconexión del entorno de ejecución. `true` hasta la primera respuesta. Consulta [Qué admite el entorno de ejecución](/docs/es/sdk/sessions#what-the-runtime-supports). |
| `useNotificationInbox({ userId, limit?, enabled? })` | La página más reciente de la bandeja de entrada de quien llama (`limit` con valor predeterminado 20), `unreadCount` y `markRead(ids)`, `markAllRead()`, `markSessionRead(sessionId)`. Sondea cada 60 s mientras la página está visible y vuelve a obtener los datos al recibir el foco. Una escritura cambia la caché de inmediato, la restaura si falla y luego vuelve a obtener cada entrada de la bandeja de entrada. No se realiza ninguna solicitud sin `userId`. |
| `useNotificationPreferences({ userId, enabled? })` | La elección de notificaciones push y correo electrónico de quien llama por tipo, `email_available` y `update(patch)` (optimista). |
| `useSessionWatch({ userId, projectId, sessionId, enabled? })` | `{ watching }` para una sesión y `setWatching(watching)` (optimista) para silenciarla o seguirla. |
| `resetRuntimeQueries(queryClient)` | No es un hook. Elimina todas las consultas del entorno de ejecución almacenadas en caché (sesiones, mensajes, agentes, comandos, diferencias). Llámalo después de que el entorno de ejecución de la sesión se sustituya mediante un reinicio o una recarga de configuración, para que la siguiente lectura provenga del nuevo entorno de ejecución. |

Los tres hooks de notificaciones usan `userId` (`qk.notifications`) como clave de caché,
por lo que un cambio de usuario en la misma pestaña nunca muestra la bandeja de entrada del usuario anterior. Pasa
el ID del usuario que ha iniciado sesión.

Las notificaciones dependen del `notification_center` por proyecto,
un [indicador de función](/docs/es/feature-flags) desactivado de forma predeterminada. Condiciona cada hook con
`enabled: useFeatureFlag(projectId, 'notification_center').enabled`, para que un
proyecto con el indicador desactivado no envíe ninguna solicitud de notificaciones.

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

## Enlaces de configuración de conectores

Un agente puede generar un enlace de conexión con un clic (`/connect/<token>`). Los datos del flujo
y la política del entorno de ejecución residen en el SDK; el anfitrión renderiza su propio diálogo sobre ellos e
inyecta sus adaptadores.

| Exportación | Función |
|---|---|
| `useConnectorSetup(token, options)` | El flujo completo: cargar lo que identifica el enlace, `POST /start`, abrir la página alojada del proveedor mediante el `openPopup` inyectado, sondear `POST /finalize` según el intervalo de 3 s / 5 s / 5 minutos e informar de `phase` (`loading`, `ready`, `starting`, `opened`, `connected`, `error`), la información de la aplicación, el error y la identidad con la que se autorizó la cuenta. `options` incluye el `backendUrl` del anfitrión, el adaptador `storage` y `openPopup`; `onOpened` se activa después de cada apertura de una ventana emergente. |
| `useConnectorLinkInfo(token, options)` | Una caché compartida y respaldada por almacenamiento por token de enlace, para que todas las tarjetas y el modal compartan un único GET de la ruta pública con límite de solicitudes y muestren el logotipo de la aplicación en el primer fotograma. Las mismas opciones `backendUrl`/`storage`. |
| `createConnectorLinkInfoCache(fetchInfo, { storage })` | La propia fábrica de caché (sin framework, con almacenamiento inyectado) para un anfitrión que quiera su propia instancia. |
| `resolveConnectorStart({ start, finalize })` | La regla de resultado de `/start`: URL alojada → ventana emergente, ya conectado → finalizar una vez, de lo contrario, el mensaje de error. |
| `nextConnectorPollDelay(attempt, elapsedMs)` | El programa de sondeo limitado (3 s al principio, 5 s después, se detiene a los 5 minutos). |

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

## Datos de conectores

`useConnectorQuery(projectId, slug, action, args, { account, enabled, staleTime })`
lee la salida de una acción de conector como una consulta en caché. `error` es un
`ConnectorCallError`: renderiza un botón de conexión a partir de `error.connectUrl` o un
selector de cuentas a partir de `error.availableAccounts`. Úsalo solo para una acción de
lectura; cada obtención ejecuta la acción y escribe un evento de auditoría. La salida se mantiene
actualizada durante 30 s, recibir el foco nunca vuelve a obtener los datos y solo se
reintenta un `429` o `503` del servicio ascendente. Consulta [Conectores desde código](/docs/es/sdk/reference#pconnectorslug--one-connector).

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

## Inicia pronto la lectura de apertura

`prefetchSessionOpen(queryClient, projectId, sessionId)` inicia la lectura de apertura de la sesión
antes de que se monte la vista de la sesión. Envía una solicitud `GET .../snapshot`
(fila de la sesión, turno, cola y los primeros 40 mensajes de la transcripción) e inicializa la
caché de filas de sesiones con ella. `useSession` usa entonces esa solicitud en lugar de
iniciar la suya propia.

Llámalo cuando una ruta identifique una sesión, o al pasar el cursor, dar el foco o tocar un enlace de
sesión. Es de solo lectura: nunca llama a `/start` ni activa un ordenador. Nunca
rechaza la promesa. Una llamada repetida para la misma sesión en un plazo de 30 segundos no envía ninguna
solicitud.

```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>

## Renderiza desde la caché al recargar

Dos almacenes conservan lo que un usuario ya ha visto, para que una recarga o un inicio en frío de la aplicación lo renderice
en el primer fotograma y lo actualice en el mismo lugar. Ambos son por usuario y tienen límites, y el
anfitrión borra ambos al cerrar sesión.

- `createPersistedQueryCache` conserva los resultados de React Query que el usuario usa para navegar:
  cuentas, proyectos, detalles del proyecto, la lista paginada de sesiones y las filas de sesiones
  (`isPersistableQueryKey`). Una entrada restaurada conserva su antigüedad original, por lo que vuelve a obtenerse
  al montar. Los límites predeterminados son 1.000.000 de caracteres y 7 días.
- `createSavedCopyStore` conserva las últimas copias guardadas de las transcripciones que envió el
  servidor. Regístralo con `setSavedCopyStore` y `useSession` muestra la copia conservada
  antes del primer fotograma. La copia del servidor se concilia entonces con ella por ID de mensaje, y
  el SDK la vuelve a leer 3 segundos después de que termine cada turno. Los límites predeterminados son 12
  sesiones, 1.500.000 caracteres y 14 días. Una copia de más de 300.000 caracteres conserva
  los mensajes más recientes que quepan; los mensajes anteriores se cargan desde el servidor cuando el usuario
  se desplaza hacia arriba.

```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 }));
}
```

En React Native, pasa AsyncStorage como `storage`. `restore` devuelve entonces una promesa.

El almacén de copias guardadas conserva solo capturas del servidor: la réplica de la transcripción que la API
escribe cuando termina un turno. La transcripción en vivo nunca se escribe en el dispositivo, por lo que un
turno detenido no puede reaparecer como uno en ejecución. Una lectura del entorno de ejecución que no devuelve mensajes
mantiene los mensajes guardados en pantalla: significa que el ordenador perdió su estado, no que la
conversación esté vacía. La primera lectura del entorno de ejecución con mensajes los concilia.

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

## Cierre de sesión y cambio de cuenta

El SDK conserva en memoria el estado de sesión por usuario: transcripciones, preguntas
y permisos pendientes, comprobantes de turnos y selecciones de modelos. Llama a `resetIdentityState()` en
cada cambio de identidad: al cerrar sesión y cuando inicie sesión un usuario diferente. Esto incluye
un inicio de sesión en otra pestaña que cambie el usuario sin cargar la página.

Llama a este controlador cuando tu flujo de autenticación inicie o cierre la sesión de un usuario.

```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()` también elimina la entrada `localStorage` del almacén del modelo. No
borra las cachés propias de la aplicación anfitriona, como React Query, ni la fuente de tokens
pasada a `createBeeuni`. Restablécelas en el mismo manejador. Nunca lanza excepciones.

Con los almacenes de [Renderizar desde la caché al recargar](#render-from-cache-on-a-reload),
bórralos en el mismo manejador: `await cache.clear()`, después
`await savedCopies.clear()` y `setSavedCopyStore(null)`. Ninguno de los dos almacenes
restaura datos de otro usuario, pero un dispositivo con la sesión cerrada no debería conservarlos.

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

## Siguiente

- [Sesiones](/docs/es/sdk/sessions) — el identificador de sesión que encapsula `useSession` y los tipos de `BeeuniSendError`.
- [Referencia](/docs/es/sdk/reference) — toda la interfaz REST de la que leen estos hooks.


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

## Historial guardado durante el inicio

`useSession(projectId, sessionId)` muestra el historial de chat guardado mientras se inicia su computadora.
El SDK lee la base de datos independientemente de si el entorno de ejecución está listo. Después, los mensajes en vivo se concilian
mediante sus ID originales. Las aplicaciones anfitrionas no necesitan una solicitud de historial ni un almacén de mensajes independientes.

El historial guardado se renderiza exactamente igual que la transcripción en vivo: texto, razonamiento y cada llamada a herramienta
con su entrada, salida y error. La vista completa de un subagente lee la transcripción guardada del propio
subagente: pasa el ámbito del padre y `savedChild: true` a
`useSessionSync(subAgentSessionId, { beeuniSessionScope, savedChild: true })`. La ventana inicial contiene los 40 mensajes más recientes, con
un máximo de 1.000.000 de caracteres. Al desplazarse hacia arriba se cargan ventanas anteriores del historial guardado, incluso mientras
la computadora está apagada. Los archivos adjuntos que requieren la computadora estarán disponibles cuando el
entorno de ejecución esté listo.

`savedTranscript` indica a la aplicación anfitriona qué renderizar mientras se inicia la computadora. Es `loading` mientras
aún puede llegar una copia guardada, `shown` cuando los mensajes están en `messages` y `none` cuando no
se puede mostrar nada antes de que responda el entorno de ejecución. Un estado desconocido es `loading`, nunca `none`.

`conversationEmpty` es verdadero cuando no hay nada que esperar. La copia guardada demuestra que la
conversación está vacía: una lectura completa del entorno de ejecución de la sesión no encontró mensajes. Desde entonces no ha terminado ningún turno
y no hay nada abierto ni en cola. La ausencia de registros nunca hace que sea verdadero, por lo que una sesión antigua
con historial nunca se muestra vacía. Una sesión obtiene esa prueba en su primer inicio con el
historial guardado activado.

```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 />;
}
```

Las llamadas a herramientas guardadas antes del 2026-09-28 perdieron su entrada y salida. El servidor sirve cada una con
la entrada que acredita su título o sus metadatos conservados: el comando y la salida de un comando, la ruta de una llamada a archivo,
el patrón de una búsqueda, la descripción de un subagente, la URL de una solicitud fetch, una lista de tareas pendientes, el nombre de una habilidad. El
siguiente inicio de la sesión vuelve a guardar todo el historial, 1:1.
<a id="pending-prompts"></a>

### Prompts pendientes

`useSessionPrompts(projectId, sessionId)` envía a la bandeja de entrada persistente de la sesión.
El servidor ejecuta automáticamente los prompts pendientes, cada uno como un turno independiente.

Pasa `placement: 'transcript'` para mostrar un mensaje pendiente en la conversación.
Se ejecuta antes de cada entrada `composer` y termina la respuesta activa después de que
finalice la llamada a herramienta actual.
Pasa `placement: 'composer'` para mantenerlo en la lista editable encima del campo de entrada.
Espera a que termine la respuesta activa. Cada ubicación mantiene el orden de envío.
Pasa `delivery: 'steer' | 'queue' | 'interrupt'` para indicar cómo llega el prompt a un
turno en ejecución. `steer` lo entrega al turno en su siguiente paso; el turno no
se detiene. `queue` espera a que termine el turno. `interrupt` termina el turno después de la
herramienta en ejecución. Con solo `delivery`, el SDK envía la ubicación que implica:
`interrupt` es `transcript`; las demás son `composer`. Una fila de redirección que no puede
redirigir muestra `delivery: 'queue'` con `steer_fallback` establecido en `unsupported`,
`not_prompter` o `turn_ended`. `interrupt(promptId)` ("Stop and send") convierte una
fila en espera en una entrada de la transcripción; una fila ya enviada responde `409`.

El editor web envía Enter como `steer` mientras se ejecuta un turno, y como un envío a la transcripción
mientras la sesión está inactiva. Command+Enter (Control+Enter en Windows/Linux)
usa la ubicación del editor. Shift+Enter inserta un salto de línea.

Las filas de la lista incluyen `full_text` para obtener vistas previas completas tras recargar. `text` sigue siendo
la vista previa limitada para clientes anteriores. Los bytes de los archivos adjuntos se excluyen de las respuestas de
lista. La eliminación devuelve el prompt completo para poder deshacerla sin pérdidas.

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

### Entrega pendiente y respuestas activas

`useSessionWorking()` mantiene `state: 'working'` mientras un prompt espera en la bandeja de entrada
persistente. Esto mantiene disponible Stop. `pendingDelivery: true` marca un prompt que aún está
llegando al entorno de ejecución; la aplicación web muestra un único indicador de actividad siempre que `state`
es `working`, independientemente de lo que indique esa bandera. Los turnos activos del entorno de ejecución y la actividad reciente del
entorno de ejecución lo borran. Una cancelación cuyo tiempo de espera se agota no confirma que la ejecución se haya detenido.

La toma de una tarea por un worker solo comprueba la admisión y mantiene el prompt en espera. La entrega comienza
después de que la admisión se complete correctamente. Un turno activo confirmado elimina la presentación de pendiente
aunque la instantánea anterior de la bandeja de entrada siga incluyendo ese prompt. La actividad del entorno de ejecución
conserva el ID de mensaje del turno activo durante esta transición.

El editor web denomina a Command/Ctrl+Enter **Queue List** y a "Stop and
send" de una fila **Quick Queue**. Las entradas de Quick Queue se ejecutan primero. Una entrada de Quick Queue en espera es una burbuja coloreada después de
los turnos entregados, sin texto de estado. Las entradas de Queue List siguen siendo editables hasta que comienza la entrega.
El servidor recupera los eventos de finalización perdidos a partir de pruebas exactas del entorno de ejecución, incluso
después de recargar, sin tratar un entorno de ejecución inaccesible como un turno completado.

Cada pulsación independiente de Enter muestra su prompt pendiente de inmediato, incluso mientras una solicitud de
aceptación anterior sigue sin resolverse. Solo un fallo de entrega añade texto: su causa,
Retry y Remove. La bandeja de entrada del servidor sigue controlando el orden de ejecución.

El hook lee los cambios de la cola desde el flujo de control de la sesión
(`GET .../sessions/:id/events?channels=control`), con una conexión por sesión
independientemente de cuántos componentes lo monten. Cada escritura de una fila de la bandeja de entrada llega como una
trama `beeuni.control.queue`. El sondeo `GET .../prompts` solo se ejecuta mientras ese
flujo no está conectado.

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

### Por qué terminó un turno

La transcripción de un turno interrumpido solo contiene `MessageAbortedError`. El
plano de control conserva lo que sabe sobre cómo terminó cada turno: la causa indicada por el
sandbox, por ejemplo `SandboxMemoryGuard`, y qué turnos terminaron sin
una causa indicada. Una detención solicitada por alguien —el botón Stop, `abort()`, un prompt
enviado a una sesión ocupada— se registra como una solicitud y nunca se informa como un
fallo.

`useSessionTurnOutcome(projectId, sessionId)` devuelve `last_ended`,
`recent_failures` y la hora de lectura `atMs` desde la caché que `useSessionWorking()`
mantiene actualizada. No realiza ninguna solicitud. Pasa el resultado a `turnEndNotice`, junto con lo que
la transcripción indica sobre el turno:

```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
```

La aplicación anfitriona asigna a cada `kind` su propio texto y nunca analiza el
mensaje del sandbox. `null` significa "mostrar lo que muestra la transcripción": un error real se mantiene; una
interrupción no renderiza nada. Un fallo sin explicación solo se informa cuando la lectura
se produce unos segundos después del final del turno, porque la causa suele llegar una trama
después de la interrupción. `turnEndCause(outcome, messageId)` devuelve la causa
registrada sin procesar, o `null`.
