---
title: "Sesiones"
description: "Ejecuta una sesión, transmite sus eventos en tiempo real y gestiona los errores que puede generar."
---

Una sesión es una ejecución de un agente, en su propio entorno aislado y en su propia rama de git.
`beeuni.session(projectId, sessionId)` devuelve el identificador para todo lo que hace una
sesión: iniciarla, enviar prompts, recibir eventos en streaming y consultar su estado. Esta
página cubre el identificador, el protocolo de confirmación de disponibilidad, el streaming y los errores
tipados que puede generar una llamada al SDK.

```ts
const s = beeuni.session(projectId, sessionId);
```

`s` es el identificador para todo lo que hace una sesión. El ID de la sesión, el ID del entorno aislado
y el nombre de la rama tienen el mismo valor. Consulta
[Sesiones](/docs/es/work/sessions) para conocer el concepto.

<a id="session-lifecycle"></a>

## Ciclo de vida de la sesión

| Método | Encapsula | Qué hace |
|---|---|---|
| `s.get(opts?)` | `GET /projects/:pid/sessions/:sid` | Consulta los detalles de la sesión |
| `s.update(input)` | `PATCH …/sessions/:sid` | Cambia el nombre de la sesión, reemplaza sus etiquetas o combina claves de metadatos |
| `s.start(waitMs?)` | `POST …/sessions/:sid/start` | Aprovisiona e inicia el entorno de ejecución |
| `s.restart()` | `POST …/sessions/:sid/restart` | Reinicia el entorno de ejecución; conserva el mismo entorno aislado |
| `s.configState()` | `GET …/sessions/:sid/config` | Consulta si la configuración está actualizada (`stale`) y el estado de la versión de configuración (`release`) |
| `s.reloadConfig(input?)` | `POST …/sessions/:sid/reload` | Recompila la configuración del agente y reemplaza el entorno de ejecución tras la validación |
| `s.reloadConfigStream(input, onEvent)` | `POST …/sessions/:sid/reload-stream` | Ejecuta la misma recarga y emite fases de progreso confirmadas por el servidor |
| `s.stop()` | `POST …/sessions/:sid/stop` | Detiene el entorno de ejecución; la sesión se conserva |
| `s.delete()` | `DELETE …/sessions/:sid` | Elimina la sesión |
| `s.setSharing(intent)` | `PUT …/sharing` | Establece las opciones de uso compartido y visibilidad |
| `s.participants()` | `GET …/sessions/:sid/participants` | Consulta quién puede abrir la sesión y quién envió cada prompt |
| `s.watch()` | `GET …/sessions/:sid/watch` | Consulta si recibes las [notificaciones](/docs/es/work/notifications) de esta sesión. Requiere `notification_center`; `403` `feature_disabled` mientras está desactivado |
| `s.setWatch(watching)` | `PUT …/sessions/:sid/watch` | Activa (`true`) o silencia (`false`) las notificaciones de la sesión para ti. Requiere `notification_center`; `403` `feature_disabled` mientras está desactivado |
| `s.presence({ tab_id, active, alerts? }, { keepalive? })` | `PUT …/sessions/:sid/presence` | Informa de una pestaña abierta. `alerts: true` indica que la pestaña muestra sus propias notificaciones, por lo que se retiene la notificación push al teléfono. Con `notification_center` desactivado, cualquier pestaña presente la retiene. `keepalive` permite que la solicitud continúe tras cerrar una página; activado de forma predeterminada para un informe de ausencia |
| `s.cost()` | `GET /usage/session-costs/:sid` | Consulta el coste definitivo del LLM y del cómputo sin iniciar el entorno de ejecución |
| `s.scope()` | `GET …/sessions/:sid/scope` | Consulta la restricción de secretos almacenada y las vinculaciones de conexión materializadas |
| `s.rescope(input)` | `PUT …/sessions/:sid/scope` | Reemplaza los campos de alcance proporcionados para el siguiente prompt o llamada a una herramienta |
| `s.commit(input?)` | — | Confirma el trabajo del agente en un commit |
| `s.providerSecretPool.get(providerId)` | `GET …/provider-secret-pools/:providerId` | Consulta los ID de los secretos de cuenta seleccionados |
| `s.providerSecretPool.list()` | `GET …/provider-secret-pools` | Enumera los grupos configurados, incluidos los vacíos, e informa de `can_edit` |
| `s.providerSecretPool.set(providerId, secretIds)` | `PUT …/provider-secret-pools/:providerId` | Reemplaza la selección; `null` restaura el valor predeterminado del proyecto |

:::warning
  `s.delete()` elimina la sesión y su entorno de ejecución. Esta acción no se puede deshacer. Para pausar una sesión
  sin perderla, llama a `s.stop()` en su lugar.
:::

<a id="labels-and-metadata"></a>

## Etiquetas y metadatos

Cada sesión incluye `labels: string[]` y un objeto `metadata`. Úsalos para
clasificar las sesiones y decidir cómo las muestra tu aplicación.

- **Etiquetas** de formato libre: se eliminan los espacios al inicio y al final de cada una, tienen entre 1 y 64 caracteres, con un máximo de 20 por
  sesión, y se descartan los duplicados. La coincidencia es exacta y distingue entre mayúsculas y minúsculas.
- **Metadatos**: un objeto JSON de formato libre para tus propias claves, con un máximo de 16.384
  caracteres de JSON por escritura. El servidor también almacena allí sus propias claves
  (`custom_name`, `source`, …); una solicitud que incluya una clave gestionada por el servidor se
  rechaza con `400`. Añade un prefijo a tus claves (`app_…`) para evitar conflictos con ellas.

```ts
// Set both at create.
const project = beeuni.project(projectId)
const created = await project.sessions.create({
  initial_prompt: 'Triage ticket T-142',
  labels: ['support', 'tier:gold'],
  metadata: { app_ticket: 'T-142', app_priority: 2 },
})

// Update later. `labels` replaces the list; `metadata` merges keys and a
// `null` value removes that key.
const s = beeuni.session(projectId, created.session_id)
await s.update({ labels: ['support', 'resolved'], metadata: { app_priority: null } })

// List by label, server-side: a session must carry EVERY given label.
const { items } = await project.sessions.listPage({ labels: ['support', 'resolved'] })

// Render conditionally.
for (const session of items) {
  const badge = session.labels?.includes('tier:gold') ? 'Gold' : null
  const ticket = session.metadata.app_ticket as string | undefined
}
```

En React, pasa `labels` a `useProjectSessions(projectId, { labels })`; cada
conjunto de etiquetas tiene su propia entrada de caché. Los agentes establecen los mismos campos desde la CLI
(`beeuni sessions new --label … --meta k=v`, `beeuni sessions update --label …`)
y desde el servidor MCP alojado (`start_session` `labels` / `metadata`,
`list_sessions` `labels`). Una versión ejecutable es `packages/sdk/examples/12-session-labels.ts`.

Con `pooled_provider_secrets` y `llm_gateway` activados, una sesión puede seleccionar
varios secretos de cuenta disponibles para un proveedor. Crea un recurso con alcance de proyecto mediante
`beeuni.accounts.secretResources.create(accountId, { project_id: projectId, ...input })`.
Todos los miembros del proyecto pueden usarlo de forma predeterminada. Restringe su uso con
`.setAccess(accountId, secretId, 'members', userIds)`. Las consultas devuelven metadatos y permisos, nunca
el valor del secreto. Tras un `429` del proveedor antes de que comience la salida, la pasarela
prueba otra clave seleccionada.

```ts
await s.providerSecretPool.set('anthropic', [primarySecretId, backupSecretId]);
const selected = await s.providerSecretPool.get('anthropic');
await s.providerSecretPool.set('anthropic', null); // inherit project default
```

Usa el método en streaming cuando quien realiza la llamada muestre el progreso de la recarga:

```ts
await s.reloadConfigStream({ refresh_repo: false }, (event) => {
  if (event.type === 'phase') console.log(event.phase);
});
```

Las fases son `checking-session`, `refreshing-workspace`, `compiling-config`,
`applying-config` y `confirming-config`. El servidor omite
`refreshing-workspace` cuando `refresh_repo` es `false`. La
fase `applying-config` incluye el reemplazo del entorno de ejecución validado por el daemon.

`s.configState()` devuelve `release` cuando la API admite versiones de configuración.
Su tipo es `SessionConfigRelease`. Los resultados de la recarga incluyen el mismo objeto.

```ts
const { stale, release } = await s.configState();
if (release?.fallback_reason) {
  // The desired config failed on the box. An earlier config runs the session.
  console.warn(release.fallback_reason, release.running_release_id);
} else if (stale === true) {
  // Moves the session onto the base branch's current config AND
  // fast-forwards its /workspace checkout. `workspace_checkout` says what
  // happened to the checkout; `detail` words both halves.
  const result = await s.reloadConfig();
  console.log(result.workspace_checkout, result.detail);
}
```

`stale` tiene tres estados. `null` significa que Beeuni no pudo determinarlo. Comprueba `stale === true`,
nunca `!stale`. Una API anterior omite `release`. Consulta
[Convergencia de la configuración](/docs/es/work/runtime#config-convergence).

`s.participants()` devuelve `{ participants, total, multi_user }`. `participants`
enumera las personas que pueden abrir la sesión ahora, con el propietario primero, hasta un máximo de 20, cada una
`{ user_id, name, email, avatar_url, is_viewer }`; `total` es el recuento total.
`multi_user` es `true` cuando dos o más personas pueden abrir la sesión. En React,
`useSessionParticipants(projectId, sessionId)` consulta los mismos datos. Quién escribió
cada mensaje se indica en `s.messageAuthors()`.

Tres métodos de consulta más completan el identificador:

- `s.previews()` — posibles puertos de vista previa que expone el entorno de ejecución.
- `s.publicShares.list()` / `.create(input)` / `.revoke(shareId)` — enlaces públicos para compartir.
- `s.reminders.list()` / `.create(input)` / `.update(reminderId, { enabled })` / `.remove(reminderId)` — prompts programados para esta sesión. Consulta [Recordatorios](/docs/es/connect/reminders).
  `.create({ transcript: true })` crea un enlace de solo lectura a la conversación (un enlace activo por sesión; una segunda llamada devuelve el mismo enlace). Léelo de forma anónima con `getPublicSessionShareMessages(token)`; `findActiveTranscriptShare(shares)` selecciona el activo de `.list()`.
- `s.audit(limit?)` — el registro de auditoría de las acciones del agente en la sesión.
- `s.attachments.upload(file, options?)` — guarda un archivo privado antes del inicio. Máximo de 50 MiB. Devuelve una URL estable para una parte de archivo de un prompt.
- `s.attachments.read(attachmentId, signal?)` — lee los bytes guardados como un `Blob` sin iniciar el sandbox.
- `s.transcript(options?)` — una transcripción compacta del lado del servidor (texto y llamadas a herramientas, sin entradas ni salidas de herramientas). Funciona con un token de sesión con ámbito de proyecto.
- `s.transcriptSync(options?)` — la transcripción que el servidor guarda al final de cada turno, en forma de sobres de mensajes de runtime. Cada parte se conserva 1:1 —texto, razonamiento y cada llamada a herramienta con su entrada, salida, error y metadatos— excepto los bytes de los adjuntos. Responde mientras el sandbox está detenido o aún se está activando, sin iniciarlo. Pasa `limit` (como máximo 500) para obtener la ventana más reciente, y el `next_cursor` de una ventana como `before` para leer la anterior. Una ventana también se limita a 1,000,000 de caracteres de JSON: conserva sus mensajes más recientes, y `next_cursor` identifica entonces el más antiguo que conservó. Pasa `child` (el id `ses_…` de un subagente, la sesión que abre una fila de subagente) para leer la transcripción guardada de ese subagente; se guarda la de cada subagente que la conversación haya enviado. `source` es `mirror`; `complete` es true solo cuando la ventana contiene todos los mensajes guardados y la copia guardada llega al primer mensaje de la sesión. Una sesión cuyo entorno de ejecución se leyó por completo y no contenía mensajes responde con `available: true`, `complete: true`, `total: 0` y ningún mensaje. Una llamada a herramienta guardada antes del 2026-09-28, cuando el historial guardado omitía las entradas y salidas de herramientas, se entrega con la entrada que su título o sus metadatos acreditan y, si es un comando, también con su salida.
- `s.voiceTranscript(options?)` — la transcripción de la llamada de voz en directo de esta sesión (turnos hablados más llamadas a herramientas de los workers `ask_beeuni`/`run_command`). Devuelve una lista vacía cuando la sesión no tiene una llamada en directo, no un 404.

<a id="files-a-session-stored"></a>

### Archivos que almacenó una sesión

`findSessionAttachments(messages)` enumera todos los archivos almacenados a los que hace referencia una transcripción,
en orden y una sola vez cada uno: un archivo subido por un usuario, una referencia `<file>` en un
prompt o la copia que el historial guardado conserva de un archivo que mostró el agente. Lee tanto
una transcripción en directo como una guardada, y nunca lanza una excepción por una transcripción mal formada.

```ts
import { findSessionAttachments } from '@beeuni/sdk'

const saved = await s.transcriptSync({ limit: 500 })
for (const file of findSessionAttachments(saved.messages)) {
  const blob = await s.attachments.read(file.attachment_id) // no sandbox needed
  console.log(file.filename, file.role, blob.size)
}
```

Cada resultado es `{ url, attachment_id, filename, mime, message_id, role }`.
`role` es `user` para un archivo subido y `assistant` para un archivo que mostró el agente;
`mime` es null para este último, y los bytes llevan su propio tipo.

<a id="who-wrote-each-message"></a>

## Quién escribió cada mensaje

Cada mensaje tiene un autor. El servidor lo obtiene de la credencial que envió el
prompt: un miembro del proyecto o el agente de otra sesión. La transcripción del entorno de ejecución no
lo incluye.

```ts
const { authors, initial_author } = await beeuni.session(projectId, sessionId).messageAuthors()
// getSessionMessageAuthors(projectId, sessionId) is the same call as a function.

const author = authors[runtimeMessageId] // undefined when the sender is unknown
```

`authors` usa como clave el id del mensaje del entorno de ejecución. `initial_author` es la sesión principal del
primer mensaje de una sesión creada. Es `null` cuando la sesión no tuvo un primer
prompt. Cada autor es un `SessionMessageAuthor`:

| `kind` | Campos |
| --- | --- |
| `'member'` | `user_id`, `name`, `email` (`string \| null`) |
| `'session'` | `session_id`, `name` (el título de la sesión), `agent` (el agente que ejecuta, si tiene nombre) |

<a id="which-model-answered-a-turn"></a>

## Qué modelo respondió a un turno

Un mensaje de la transcripción indica el modelo que solicitó su turno. Cuando la
[cadena de respaldo](/docs/es/project/models) del proyecto responde desde otro modelo, solo el registro de
solicitudes de la puerta de enlace contiene el modelo que respondió y lo que Beeuni facturó.

```ts
const { latest, billed_cost, turns } = await beeuni.session(projectId, sessionId).modelUsage()
// getSessionModelUsage(projectId, sessionId) is the same call as a function.

latest?.served_model   // 'glm-5.3-flash': the model that answered the newest request
latest?.fallback_from  // 'codex/gpt-6.1-sol' when a fallback model answered, else null
turns[userMessageId]   // { served_models, fallback_from, billed_cost } for one turn
```

`latest` es `null` antes de la primera respuesta. `billed_cost` es el importe en USD que Beeuni descontó por
las llamadas a modelos de la sesión. `turns` usa como clave el id del mensaje del entorno de ejecución del prompt que
inició cada turno. `served_models` enumera los modelos que respondieron en ese turno, primero los que
recibieron más solicitudes. En React, `useSessionModelUsage(projectId, sessionId)` lee el mismo
registro; llama a su `refetch()` cuando termine un turno.

En React, `useSessionMessageAuthors(projectId, sessionId, messageCount)` lee los
mismos datos. Pasa el número de mensajes de la transcripción. Un mensaje nuevo es el único evento que
añade un autor, así que la consulta vuelve a obtener los datos cuando cambia el número.

<a id="readiness-is-a-handshake"></a>

## La disponibilidad requiere un protocolo de enlace

Antes de enviar un prompt, llama a `ensureReady()`. Aprovisiona el sandbox si
es necesario, espera a que arranque el entorno de ejecución y devuelve el entorno de ejecución resuelto.

```ts
const { runtimeSessionId, runtimeUrl, sandboxId } = await s.ensureReady();
```

En un arranque en frío, `ensureReady()` puede lanzar `RUNTIME_UNAVAILABLE`. Consulta
[Reintentar en un arranque en frío](#retry-on-a-cold-boot) para saber qué significa y cómo
reintentarlo.

`s.send()` y `s.abort()` llaman a `ensureReady()` por ti.

<a id="seed-a-server-authorized-runtime-session-pin"></a>

### Inicializar una fijación de sesión del entorno de ejecución autorizada por el servidor

Un host de React renderizado en el servidor puede proporcionar la fijación de sesión del entorno de ejecución ya persistida para
la misma sesión de Beeuni:

```tsx
const session = useSession(projectId, sessionId, {
  initialRuntimeSessionId: persistedSession.runtime_session_id ?? persistedSession.opencode_session_id,
});
```

El valor inicial solo hidrata el contenido de la transcripción en caché mientras se ejecuta `/start`. No
anula la identidad del entorno de ejecución. La fijación que devuelve `/start` es
la fuente de autoridad y sustituye un valor inicial obsoleto.

`opencode_session_id` es el nombre obsoleto de `runtime_session_id`. Ambos
contienen el mismo valor; lee el nombre antiguo solo como alternativa de respaldo indicada arriba.

No aceptes este valor de un selector de tenant no confiable. No crees una
sesión del entorno de ejecución en el host. Beeuni crea y persiste la sesión raíz.
Las cachés de consultas del entorno de ejecución y los controladores de transcripción tienen como ámbito el entorno de ejecución
del sandbox, por lo que los ids de sesión del entorno de ejecución iguales de distintos sandboxes no comparten
entradas de caché.

Los reintentos de mensajes pendientes conservan la URL de su sandbox de origen cuando cambias de
sesión. Una respuesta de mensaje `404` o `410` detiene los reintentos automáticos sin
borrar la transcripción. Una reconciliación explícita puede reintentar la lectura y
restaurar la sincronización normal tras una respuesta correcta.

<a id="send-a-prompt"></a>

## Enviar un prompt

```ts
s.setModel({ providerID, modelID }); // sticky for later send() calls
s.setAgent('build'); // sticky for later send() calls

await s.send('Refactor the auth module');
await s.send('One-off task', { model, agent }); // overrides for this call only
await s.abort(); // stop the current run
```

`send()` coloca el prompt en la bandeja de entrada duradera de prompts de la sesión
(`POST .../prompts`), la vía que usan el editor web y la CLI. Se resuelve
cuando se almacena el prompt, con la fila de la bandeja de entrada (`prompt_id`, `state`,
`message_id`), no cuando termina el turno. Lee la respuesta de `s.stream()` o de la
transcripción.

Para las sesiones REST de runtime, el primer `send()` en un handle lee el modelo y
el agente persistidos en la sesión de Beeuni. Esto evita que una sesión de
runtime heredada de una instantánea reutilice valores predeterminados obsoletos de la instantánea.

La precedencia de selección del prompt es:

1. La llamada a `send()`.
2. El valor `setModel()` o `setAgent()` del handle.
3. El valor predeterminado persistido de la sesión de Beeuni.

`setModel` solo elige lo que solicita el siguiente `send` local: nunca sale del
handle. Para **persistir** un modelo nuevo para una sesión en ejecución del lado del servidor, usa
`changeModel`:

```ts
const { applied_live } = await s.changeModel('anthropic/claude-opus-4-8');
```

El cambio se aplica al reiniciar el entorno de ejecución, por lo que un turno en curso termina.
`applied_live` es `true` cuando una sesión en ejecución lo aplicó en ese momento, y `false` cuando
se aplica en el siguiente inicio. Solo el propietario de la sesión o quien haga la llamada con permisos de administrador
del proyecto puede cambiar el modelo; cualquier otra persona recibe `403`.

`send()` resuelve el entorno de ejecución y después le envía el prompt. `abort()` detiene la ejecución
actual sin eliminar la sesión.

<a id="session-scope-and-cost"></a>

## Ámbito y coste de la sesión

Lee la restricción de secretos almacenada y los enlaces de conexión materializados.
`secrets_allowlist: null` significa que se aplica la concesión de secretos del agente:

```ts
const scope = await s.scope();
scope.connector_bindings_configured; // false = inherits the project defaults
```

`connector_bindings` es el mapa RESUELTO, por lo que tiene el mismo aspecto para una sesión
que haya sobrescrito sus conectores y para otra que herede los valores predeterminados del proyecto. Lee
`connector_bindings_configured` para distinguirlas antes de mostrar el alcance o
devolverlo.

Reemplaza uno o ambos campos de alcance:

```ts
await s.rescope({
  secrets: ['DATABASE_URL'],
  connector_bindings: {
    github: { connection_id: connectionId },
  },
});
```

Cada campo proporcionado reemplaza por completo su valor anterior. Omite un campo para dejarlo
sin cambios. Los cambios de conexión se aplican a la siguiente llamada a una herramienta.
Eliminar un secreto impide su entrega futura, pero no puede eliminar un
valor ya revelado del contexto del modelo ni de un proceso existente.

Ambos ejes tienen una forma explícita de volver al valor predeterminado. No equivalen a un
valor vacío:

```ts
await s.rescope({
  secrets: null, // inherit the agent's secret grant
  connector_bindings: null, // drop the override; inherit the project defaults
});
```

`secrets: []` y `connector_bindings: {}` son la instrucción opuesta: un
«sin secretos del proyecto» y «sin ningún conector» explícitos, incluidos los valores predeterminados
del proyecto. Una sesión que envía `{}` cuando quería enviar `null` deniega por defecto todos los
alias que no haya nombrado.

Lee el registro unificado de costes:

```ts
const cost = await s.cost();
```

El registro combina el coste finalizado del LLM, el coste facturado del cómputo del entorno aislado, el uso
del modelo, los totales de tokens, la duración del cómputo y los asientos contables. `s.cost()` no
llama a `ensureReady()`.

<a id="runtime-status-and-previews"></a>

## Estado del entorno de ejecución y vistas previas

| Método                      | Devuelve                        | Uso                                          |
| --------------------------- | ------------------------------ | -------------------------------------------- |
| `s.health(init?)`           | `{ status, ok, health, body }` | Comprobar si el entorno de ejecución está activo           |
| `s.previewUrl(port, path?)` | `string`                       | Obtener una URL de proxy para un puerto que el agente haya expuesto |
| `s.proxyUrl(url?)`          | `string \| undefined`          | Reescribir una URL de localhost que el agente haya mostrado    |

```ts
const { ok, health } = await s.health();
const url = s.previewUrl(3000, '/docs');
```

`s.health()` nunca lanza excepciones. Llámalo en cualquier momento, incluso antes de que la sesión tenga un
entorno de ejecución. `s.previewUrl()` y `s.proxyUrl()` necesitan un entorno de ejecución resuelto: llama
primero a `s.ensureReady()` o lanzarán `SessionNotReadyError`. Consulta
[Errores de preparación de la sesión](#session-readiness-errors).

<a id="what-the-runtime-supports"></a>

### Qué admite el entorno de ejecución

`health.capabilities` enumera las funciones de sesión que ofrece el entorno de ejecución. Consulta una
con `runtimeSupports(capabilities, capability)` y oculta el control cuando
devuelva `false`. Una sesión de runtime ofrece las diez; una sesión de pi ofrece
solo `session.subagents` y responde a las demás con `501 feature_not_supported`
(`404` para el documento de configuración). [Harnesses](/docs/es/work/harnesses) contiene la
matriz completa de runtime y pi.

| Capacidad | Función |
| --- | --- |
| `session.rewind` | Revertir a un mensaje y restaurarlo |
| `session.compact` | Resumir la conversación bajo demanda |
| `session.commands` | Comandos de barra del proyecto |
| `session.fork` | Bifurcar una sesión en un mensaje |
| `session.subagents` | Sesiones hijas de subagentes |
| `session.mcp` | Servidores MCP a los que se conecta el propio entorno de ejecución |
| `session.todo` | La lista de tareas del entorno de ejecución |
| `session.shell` | Un comando de shell ejecutado como un turno |
| `session.attach` | Conectar el cliente de terminal propio del harness |
| `session.config` | Un documento de configuración del entorno de ejecución (`/global/config`); los agentes y modelos proceden del proyecto en ambos casos |

```ts
import { runtimeSupports } from '@beeuni/sdk';

const { health } = await s.health();
if (!runtimeSupports(health?.capabilities, 'session.compact')) hideCompact();
```

`runtimeSupports` devuelve `true` antes de que llegue una respuesta de estado y para un
entorno de ejecución que no enumere ninguna capacidad `session.*`: un demonio creado antes de que existieran las capacidades
ejecuta runtime, que ofrece todas las funciones. En React, usa
[`useRuntimeSupports`](/docs/es/sdk/react).

La respuesta de `start` también incluye la lista: `SessionStartResult.capabilities` se
establece con `stage: 'ready'`. `useSession` la registra, de modo que la comprobación de capacidades es
correcta antes de que responda la primera consulta de estado.

<a id="status-words"></a>

## Términos de estado

Todos los hosts usan el mismo vocabulario, de modo que cada estado tiene el mismo término en todas las pantallas.

| Exportación                                     | Uso                                                                                          |
| ------------------------------------------ | -------------------------------------------------------------------------------------------- |
| `sessionListStatus(session, reviewCount?)` | Una sesión en una lista: `needs-you`, `starting`, `running`, `done`, `stopped`, `failed`, `legacy` |
| `SESSION_LIST_STATUS[status]`              | `{ label, tone, description }` para ese estado                                               |
| `sessionConnectionLabel(connection)`       | Una etiqueta breve para el ordenador de la sesión, o `null` para `unknown` y `live`                  |
| `SESSION_NOTICE`                           | La frase del editor de mensajes mientras el ordenador no esté listo                                      |
| `turnRetryLabel(secondsLeft)`              | `Retrying in 5s`, `Retrying now` o `Waiting to retry` cuando no se conoce la cuenta atrás           |

Los tonos son `actionable`, `live`, `progress`, `muted` y `danger`. El verde es
solo para `live` y `actionable`; una sesión finalizada es `muted`. Un estado que envíe una
API más reciente y que esta compilación no conozca se muestra como `stopped`, nunca como `failed`.

## Streaming

Usa `s.stream()` para recibir eventos en directo en un script o servidor. En una aplicación
React, usa [`useSession`](/docs/es/sdk/react) en su lugar: gestiona todo el
ciclo de vida de la sesión por ti.

`s.stream()` es el flujo de eventos de compatibilidad REST de runtime. La API de Beeuni
lo transmite mediante proxy desde el entorno aislado. No hay un endpoint WebSocket independiente. El
transporte es `fetch` con un cuerpo de respuesta en streaming, que se lee mediante
`ReadableStream` y `TextDecoderStream`. El SDK gestiona la reconexión,
la espera progresiva y la comprobación de latidos.

Transmite una sesión en streaming:

1. Llama primero a `ensureReady()`. El entorno de ejecución no existe hasta que se inicia el
   entorno aislado.
2. Abre el flujo antes de enviar un mensaje para no perderte los primeros
   eventos.
3. Envía el mensaje.
4. Cierra el flujo cuando veas `session.idle`.

```ts
const session = beeuni.session(projectId, sessionId);
const { runtimeSessionId } = await session.ensureReady();

const stream = await session.stream({
  onEvent: (event) => {
    if (event.type === 'session.idle' && event.properties.sessionID === runtimeSessionId) {
      onTurnDone();
      stream.close();
    }
  },
});

await session.send('Refactor the auth module');
```

El flujo entrega eventos en lotes, separados por 16 ms. Los eventos
`message.part.delta` consecutivos para un mismo campo de una parte dentro de un lote llegan como un único evento.
Su `properties.delta` es el texto de esos eventos, unido en orden. Su `id` es el id del último
evento. Su matriz `coalesced` enumera los eventos que reemplazó. Añade `delta`
al campo de la parte, como harías con un único delta.

El streaming necesita `fetch` con un cuerpo `ReadableStream` real y
`TextDecoderStream`. Los navegadores, Node 18 y versiones posteriores, Bun y Cloudflare Workers
lo admiten.

Un host cuyo `fetch` no pueda transmitir en streaming proporciona su propio transporte. El SDK lo llama
una vez por conexión y sigue gestionando la reconexión, la reanudación de `Last-Event-ID`, la espera progresiva y
los latidos:

```ts
import { configureBeeuni, notifyHostSignal, type RuntimeEventTransport } from '@beeuni/sdk';

const transport: RuntimeEventTransport = async function* ({ url, headers, signal }) {
  // Yield { data, id?, retry? } for each server-sent message of ONE connection.
  // Return when the server ends it. Throw (with `status` for a non-2xx answer)
  // when it fails.
};

configureBeeuni({ backendUrl, getToken, eventStreamTransport: transport });

// A host without `visibilitychange` / `online` events reports them itself.
notifyHostSignal('visible'); // the app is back in the foreground
notifyHostSignal('online');  // the network is back
notifyHostSignal('retry');   // a person asked to reconnect now
```

React Native no tiene `fetch` en streaming de forma predeterminada. La aplicación móvil de Beeuni usa
este punto de integración con `react-native-sse`. `createHttpSessionSyncController` sigue
estando disponible para un host que solo necesite un historial acotado y sincronización
de estado.

El controlador carga primero los 10 mensajes más recientes. `loadOlder()` sigue el
cursor del servidor. `loadHttpSessionHistory()` sigue todos los cursores para las
exportaciones explícitas.

<a id="event-types"></a>

### Tipos de eventos

Cada evento tiene un `type` y un objeto `properties` que contiene sus datos, por
ejemplo `event.properties.sessionID`.

| `type`                                          | Cuándo se activa                                    |
| ----------------------------------------------- | --------------------------------------------------- |
| `message.updated` / `message.removed`           | Un mensaje cambió o se eliminó.                     |
| `message.part.updated` / `message.part.removed` | Una parte (texto, llamada a herramienta, archivo) creció o se eliminó. |
| `session.status`                                | Cambió el estado de actividad de la sesión.          |
| `session.idle`                                  | El turno terminó.                                   |
| `session.error`                                 | El turno falló. El evento contiene el error.         |
| `question.asked`                                | El agente solicitó información.                     |
| `question.replied` / `question.rejected`        | Llegó la respuesta a una pregunta.                  |

Convierte mensajes y partes sin procesar en contenido renderizable con `classifyTurn`. Consulta
[Referencia del SDK](/docs/es/sdk/reference).

<a id="retry-on-a-cold-boot"></a>

## Reintentos en un arranque en frío

`ensureReady()` consulta el endpoint `/start` de la sesión —cada llamada usa sondeo largo de hasta
30 s— hasta que el entorno de ejecución alcanza una etapa terminal `ready`/`failed`/`stopped`
o vence su plazo (`readyTimeoutMs`, ~180 s de forma predeterminada). En una sesión ya activa,
el primer sondeo resuelve `ready` inmediatamente. En un arranque en frío, sigue consultando
mientras el sandbox indica `retriable: true`, por lo que un inicio lento simplemente tarda más
en lugar de lanzar una excepción. Solo lanza un `ApiError` con `code:
'RUNTIME_UNAVAILABLE'` if the runtime is still not `ready` cuando vence el plazo.

`ensureReady()` es idempotente, así que las llamadas simultáneas para la misma sesión comparten
una solicitud `/start` en lugar de enviar varias. La función auxiliar `retryUntilReady`
de abajo ahora es opcional —`ensureReady()` ya reintenta internamente—, pero sigue
siendo útil si quieres un tiempo total mayor que el `readyTimeoutMs` predeterminado.

```ts
async function retryUntilReady<T>(ensure: () => Promise<T>): Promise<T> {
  const deadline = Date.now() + 300_000;
  for (;;) {
    try {
      return await ensure();
    } catch (error) {
      const provisioning = error instanceof ApiError && error.code === 'RUNTIME_UNAVAILABLE';
      if (!provisioning || Date.now() > deadline) throw error;
      await new Promise((r) => setTimeout(r, 3_000));
    }
  }
}
```

Consulta [Clases de error](#error-classes) para ver la estructura completa de `ApiError`. En React,
[`useSession`](/docs/es/sdk/react) reintenta `/start` por ti, así que no necesitas
este patrón.

<a id="what-start-tells-you"></a>

## Qué te indica `/start`

Cada respuesta de `/start` describe **esa llamada**, no el historial acumulado
de la fila. Cuatro campos contienen esa información.

| Campo | Significado |
| --- | --- |
| `observed_at` | Un único reloj para toda la respuesta. |
| `action` | Lo que hizo el servidor: `inspected`, `checked_provider`, `resumed`, `provisioned`, `restored`, `reconciled`, `awaited_wake`, `cooling_down`. |
| `observation` | Lo que comprobó el servidor. `known: false` significa **no comprobado en esta llamada**, nunca «comprobado sin encontrar nada». |
| `boot` | `phase` (`provisioning` / `resuming` / `booting` / `ready` / `parked` / `failed`), `since` y `actively_starting`. |

`boot.actively_starting` responde a «¿se está ejecutando una operación del proveedor para esta
sesión ahora mismo?». Un payload `starting` con `actively_starting: false` significa
que el servidor está esperando a que termine el periodo de espera para reintentar, no que una máquina esté arrancando.

```jsonc
{
  "stage": "starting",
  "retriable": true,
  "reason": "runtime_wake_cooldown",
  "observed_at": "2026-08-26T14:00:00.000Z",
  "action": "cooling_down",
  "boot": { "phase": "resuming", "since": "2026-08-26T13:58:00.000Z", "actively_starting": false },
  "observation": {
    "provider": { "known": false, "status": null, "checked_at": null },
    "runtime": { "known": false, "state": null, "boot_phase": null, "checked_at": null }
  },
  "failure": {
    "category": "sandbox-provider",
    "message": "The runtime did not start (attempt 2). Retrying automatically.",
    "retryable": true,
    "evidence": {
      "check": "provider_not_running",
      "observed_at": "2026-08-26T13:58:00.000Z",
      "error": null,
      "attempts": 2,
      "next_retry_at": "2026-08-26T14:03:00.000Z"
    }
  }
}
```

<a id="a-failed-start-is-retried-for-you"></a>

### Un inicio fallido se reintenta por ti

Un inicio que falla establece un **periodo de espera**, no un resultado permanente. La siguiente
llamada a `/start` tras ese periodo vuelve a intentar la activación automáticamente. El periodo de espera aumenta
con los fallos consecutivos (2 min, 5 min, 10 min). Tras cinco fallos consecutivos,
`/start` responde `stage: "failed"` con el número de intentos en
`failure.message`; ese resultado caduca 30 minutos después del último fallo, y
`POST …/restart` lo borra inmediatamente.

`retriable` se calcula en cada llamada. Un estado que el servidor aún puede reintentar
nunca contiene `retriable: false`.

`failure.evidence` indica la comprobación que produjo el resultado negativo, cuándo se ejecutó y
cuándo reintentará el servidor. Cada fallo de `/start` lo contiene.

<a id="prompt-attachments"></a>

## Archivos adjuntos al prompt

Sube archivos locales cuando los usuarios los añadan al editor de mensajes. Las subidas privadas pertenecen
al proyecto y no requieren una sesión en ejecución.

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

const uploads = usePromptAttachments(projectId);
// File picker, paste, and drop handlers call uploads.add(file) or uploads.addMany(files).
// Render uploads.attachments. Refuse Send only while an attachment is `error` or `aborted`.

async function submit() {
  const ids = uploads.attachments.map((item) => item.id);
  uploads.submit(ids); // Send never waits: the composer clears and uploads continue.
  paintMessage(text, ids);
  try {
    const parts = await uploads.whenReady(ids);
    await beeuni.session(projectId, sessionId).prompts.create({
      clientMessageId,
      messageId,
      parts: [{ type: 'text', text }, ...parts],
    });
    uploads.forget(ids);
  } catch {
    uploads.reclaim(ids);
  }
}
```

`whenReady(ids, { signal })` se resuelve cuando todas las subidas están listas, en el orden de `ids`.
Se rechaza cuando una subida falla, se cancela o se elimina, o cuando `signal` se cancela.
`submit(ids)` entrega las entradas a ese envío: salen de `attachments`, dejan
de contar para los límites y siguen subiendo después de que el editor de mensajes se desmonte.
Llama a `forget(ids)` cuando el POST se complete correctamente, o a `reclaim(ids)` para devolverlas
al editor de mensajes tras un envío fallido.
La parte de archivo resuelta contiene `attachment_id`, `filename` y `mime`.
No contiene ninguna URL ni bytes del archivo. El servidor vincula y materializa la subida.
Cada archivo llega al ordenador de la sesión, incluidas las imágenes y los PDF: antes de
que se ejecute el prompt, el servidor lo escribe en `/workspace/uploads/.beeuni-inbox/` y el
prompt lo referencia mediante su ruta. El propio prompt no transporta bytes del archivo. Con
el historial guardado activado, el servidor también conserva una copia para la transcripción. Un fallo al copiar
nunca impide que un archivo llegue al ordenador. Un archivo que el servidor no puede escribir bloquea el
prompt, que se reintenta.
Usa estas partes al crear sesiones en la plataforma, iniciar sesiones ya activas o en las rutas
de la bandeja de entrada de prompts. No pases identificadores privados directamente a `sendParts` del entorno de ejecución de runtime.
Las partes de archivo heredadas de la plataforma con `url` siguen funcionando.

Cada elemento tiene un `id` estable, la referencia `file` original, `receivedBytes` y
un estado: `pending`, `uploading`, `processing`, `ready`, `error` o `aborted`.
Los bytes enviados determinan el progreso. El controlador publica una instantánea del progreso solo
cuando cambia el porcentaje entero, como máximo diez por segundo por subida. `processing` puede
permanecer al 100 % hasta que el servidor complete la subida. Los límites son 50 MiB por
archivo, 100 MiB por mensaje y 20 archivos. Los archivos no deben estar vacíos. De forma predeterminada,
se suben dos archivos simultáneamente.

`retry(id)` reanuda el mismo identificador. Después de `attachment_size_mismatch` o
`attachment_failed`, el servidor no conserva ningún identificador utilizable, por lo que `retry(id)` vuelve a subir el
File como un nuevo adjunto. Una subida caducada no se puede reintentar: `retry(id)`
lanza una excepción y el error del elemento contiene el código `attachment_expired`. `abort(id)` cancela
el trabajo sin terminar.
`remove(id)` elimina la entrada, cancela su subida y se resuelve de inmediato. El
DELETE del servidor se realiza en la medida de lo posible y nunca se rechaza; una subida que no pueda eliminar
caduca después de 24 horas.
`forget(ids)` libera las entradas sin eliminar los objetos del servidor.
El hook cancela el trabajo listado al desmontarse o cambiar de proyecto e intenta eliminar sus subidas
en la medida de lo posible: ningún envío las retiene y los borradores no conservan ningún identificador. Las subidas entregadas
continúan.

Las selecciones solo existen en memoria. Nunca guardes objetos File, URL de blob, URL
firmadas ni identificadores de subida en un borrador. Las subidas abandonadas caducan después de 24 horas.

Los hosts sin framework usan `beeuni.project(projectId).attachments.createController()`
con los mismos métodos. Suscríbete mediante `subscribe(listener)` y lee
`getSnapshot()`. Llama a `dispose()` cuando se cierre el editor de mensajes.
Quienes hagan llamadas directas pueden usar `attachments.upload(file, { signal, onProgress, onUpload,
resume })` and `attachments.delete(attachmentId)`.
Conserva los metadatos de `onUpload` para reanudar manualmente con el mismo ID.

El servidor selecciona el transporte en el campo `upload` del identificador:

- `kind: 'direct'` es el valor predeterminado. El SDK envía el archivo completo en un solo `PUT` a
  `upload.url` con `upload.headers` y sin encabezado Authorization. Con
  `XMLHttpRequest` (navegadores, React Native), el progreso indica los bytes enviados. Con
  `fetch`, el progreso indica 0 y luego el tamaño completo. Una URL caducada, o una que
  Storage rechaza con 400, 401 o 403, se vuelve a firmar una vez para el mismo
  `attachment_id`. Un `409` de Storage significa que un intento anterior almacenó el archivo.
- `kind: 'chunked'` envía solicitudes `PUT` autenticadas y secuenciales de `upload.chunk_size`
  bytes. El SDK acepta cualquier `chunk_size` positivo. Solo un despliegue cuya capa perimetral
  descarta cuerpos de solicitud grandes selecciona este modo.

La finalización verifica el tamaño almacenado y el SHA-256. Si la finalización responde `409
attachment_not_uploaded`, `onUpload` informa del identificador directo con
`received_bytes: 0`, por lo que `retry(id)` o una reanudación vuelve a enviar el archivo.
El inicio, la carga y la finalización reintentan los tiempos de espera agotados, los errores de red, 429 y
5xx con espera exponencial y variación aleatoria. El margen es de 60 segundos desde el primer
fallo, por lo que una carga larga que falla al final sigue reintentándose. La finalización también reintenta
`attachment_processing`, con un margen de cinco minutos. El inicio nunca reintenta 402
(un `BillingError`: la cuenta no puede ejecutarse) ni 429 `attachment_budget_exceeded` (40
cargas sin terminar o 500 MiB de cargas sin enviar para el usuario; las cargas no utilizadas
caducan en un plazo de 24 horas). El servidor responde o rechaza una finalización en un máximo de 105
segundos. Cada solicitud de finalización permite 120 segundos. Las cancelaciones del llamante nunca se reintentan.

La referencia de un archivo adjunto enviado se libera 1 hora después de que se entregue su prompt,
y cuando se elimina su sesión o proyecto. El siguiente barrido de mantenimiento
elimina el archivo, y su `attachment_id` ya no se puede enviar.

<a id="files"></a>

## Archivos

`s.files` lee y escribe en el sandbox de la sesión: `list`, `read`, `readBlob`,
`status`, `findFiles`, `findText`, `upload`, `create`, `copy`, `remove`,
`mkdir`, `rename`. Cada llamada resuelve primero el entorno de ejecución y siempre se dirige
al sandbox propio de esta sesión. Consulta la [referencia del SDK](/docs/es/sdk/reference) para
ver la lista completa de métodos.

<a id="session-verbs"></a>

## Verbos de sesión

El identificador lee y responde al entorno de ejecución por ti. Cada verbo aprovisiona
primero la sesión y se dirige al sandbox propio de esta sesión, sea cual sea el harness que la ejecute.

```ts
// A page of the transcript, oldest first: { info, parts } messages (beeuni.transcript.v1).
const { messages, hasMore } = await s.messages({ limit: 50 });
// What the agent waits on.
const { statuses, permissions, questions } = await s.pending();
await s.answerPermission(permissions[0].id, 'once'); // 'once' | 'always' | 'reject'
await s.answerQuestion(questions[0].id, [['Yes']]); // null dismisses the question
// Summarize the conversation. Needs the `session.compact` capability (see health()).
await s.compact();
```

`s.runtime`, el cliente de compatibilidad REST sin procesar, está obsoleto. Usa los verbos
anteriores con `send`, `abort`, `rewind` y `stream`.

<a id="warm-a-project-session"></a>

## Prepara una sesión de proyecto

Llama a `ensureWarm()` cuando la página de inicio de un proyecto necesite un entorno de ejecución listo antes
del primer prompt.

```ts
const project = beeuni.project(projectId);
const warm = await project.sessions.ensureWarm();

// An ORDINARY session. Prompt it like any other.
await beeuni.session(projectId, warm.session.session_id).send("Build me a widget");
```

`ensureWarm()` crea o devuelve una sesión no utilizada para el usuario actual. Es
la misma creación que ejecuta `sessions.create()`, con los valores predeterminados del proyecto: el mismo
control de facturación y los mismos requisitos de conectores. La única
diferencia es `metadata.warm`, que oculta la sesión de
`sessions.list()` hasta que llega su primer prompt.

Trátala como especulativa. Un `409 WARM_SESSION_UNAVAILABLE` significa que el proyecto
no se puede preparar ahora mismo; continúa con `sessions.create()`, que informa
del motivo real.

La sesión preparada lleva el agente DEFAULT y el sandbox del proyecto. Si el usuario
elige otro, abandónala y llama a `sessions.create()`: una sesión preparada sin utilizar
permanece oculta y se elimina automáticamente.

:::warning
`claimWarm()` está obsoleto. Una sesión preparada es una sesión normal, así que no hay
nada que reclamar: navega hasta ella y envíale un prompt. La llamada sigue funcionando para
los consumidores que usan la estructura anterior y se eliminará en la siguiente versión principal.
:::

<a id="handling-errors"></a>

## Gestión de errores

Cada llamada a través de `createBeeuni` se rechaza con una subclase tipada de `Error`,
nunca con un objeto simple. Captura el error, comprueba `instanceof` y decide cómo proceder según
`.status` o `.code`.

```ts
import { ApiError, BillingError } from '@beeuni/sdk';

try {
  await beeuni.project(projectId).sessions.create();
} catch (err) {
  if (err instanceof BillingError) {
    // 402 — out of credits or over a plan limit
  } else if (err instanceof ApiError) {
    // any other failed request — err.status, err.code, err.detail
  } else {
    throw err;
  }
}
```

<a id="error-classes"></a>

### Clases de error

| Clase                  | Hereda de    | Cuándo se lanza                                                                 | Campos clave                                                           |
| ---------------------- | ---------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| `ApiError`             | `Error`    | Predeterminada para cualquier solicitud fallida: estado incorrecto, fallo de red, tiempo de espera agotado o cancelación | `status`, `code`, `detail`, `response`, `url`, `endpoint`, `timeout` |
| `AuthError`            | `ApiError` | `getToken` devolvió `null`. Beeuni nunca envió la solicitud                      | `code` siempre es `'NO_SESSION'`                                      |
| `BillingError`         | `ApiError` | HTTP `402`. La única clase de error de facturación                                       | `status` (`402`), `code`, `detail.message`                           |
| `RequestTooLargeError` | `ApiError` | HTTP `431`. Normalmente, demasiados archivos en una solicitud                              | `detail.suggestion`                                                  |
| `SessionNotReadyError` | `Error`    | Se ejecutó un accesor del entorno de ejecución antes de `ensureReady()`                                  | `name` es `'SessionNotReadyError'`                                   |

`ApiError.name` es `'ApiError'` de forma predeterminada. Dos casos lo sobrescriben:

- `name: 'AbortError'`, `code: 'ABORTED'`: la solicitud se canceló, por ejemplo, al navegar. Esto no es un fallo. Ignóralo.
- `code: 'TIMEOUT'`: se agotó el tiempo de espera de la propia solicitud. `url`, `endpoint` y `timeout` muestran qué agotó el tiempo de espera.

Para cualquier otro fallo, `status` contiene el código de estado HTTP. `code` proviene del `error_code` del backend o, en su defecto, del estado como cadena. `message` es una propiedad propia enumerable de `ApiError`, por lo que se conserva con `JSON.stringify` y la expansión de objetos.

Beeuni vuelve a enviar una solicitud que recibe `401` con un token nuevo de `getToken()`, cuando ese token difiere del que envió. Una solicitud con un cuerpo de flujo de un solo uso no se vuelve a enviar.

Beeuni reintenta algunas solicitudes antes de que tu código vea un error. Si una solicitud `GET` o `HEAD` devuelve `502`, `503` o `504`, Beeuni la reintenta hasta 2 veces, con una espera de 250 ms y luego de 500 ms. Un fallo transitorio de transporte en una `GET` o `HEAD` —un error de red, no un código de estado— se reintenta de la misma forma. Un reintento que tiene éxito nunca llega a `onError`. Beeuni nunca reintenta solicitudes `POST`, `PUT`, `PATCH` o `DELETE`, ni una respuesta `500`.

Beeuni lanza `AuthError` en el cliente, antes de enviar una solicitud, cuando `getToken()` devuelve `null`. `AuthError` hereda de `ApiError`, por lo que `err instanceof ApiError` sigue coincidiendo. Comprueba `err instanceof AuthError` o `err.code === 'NO_SESSION'` para tratar «no ha iniciado sesión» como un caso distinto de un fallo del backend.

Beeuni lanza `BillingError` para cada respuesta HTTP `402`: sin créditos, por encima de un límite del plan u otro control de facturación. `detail.message` contiene el motivo del backend y `code` contiene el código de máquina del backend (por ejemplo, `app_account_unfunded`). `BillingError` hereda de `ApiError`, así que comprueba `BillingError` primero cuando gestiones ambos.

Beeuni lanza `RequestTooLargeError` para HTTP `431`. Esto normalmente significa que la solicitud llevaba demasiados archivos. `detail.suggestion` contiene una indicación lista para mostrar al usuario.

<a id="session-readiness-errors"></a>

### Errores de disponibilidad de la sesión

Dos errores significan que el sandbox de la sesión aún no está listo. Gestiona cada uno de forma distinta.

`SessionNotReadyError` lanza una excepción de forma síncrona cuando llamas a un accessor de runtime — `session.previewUrl()` o `session.proxyUrl()` — antes de que este identificador de sesión haya resuelto su sandbox. Un identificador de sesión solo resuelve su propio sandbox; nunca recurre al sandbox de otra sesión.

```ts
import { SessionNotReadyError } from '@beeuni/sdk';

const s = beeuni.session(projectId, sessionId);
try {
  const url = s.previewUrl(3000); // throws: not resolved yet
} catch (err) {
  if (err instanceof SessionNotReadyError) {
    await s.ensureReady();
  }
}
```

Llama primero a `await session.ensureReady()` o llama a `send()`, que prepara la sesión internamente. `session.health()` es el único accessor que nunca lanza este error, por lo que puedes consultarlo antes de que se inicie la sesión.

`RUNTIME_UNAVAILABLE` es el segundo error: significa que `ensureReady()` agotó el tiempo de espera mientras esperaba un arranque en frío. Consulta [Reintentar durante un arranque en frío](#retry-on-a-cold-boot) para ver el patrón completo. En React, `useSession` lo reintenta por ti y lo expone mediante el valor `phase` en lugar de lanzar una excepción.

### Helpers

| Helper                           | Firma                           | Qué hace                                                                                                  |
| -------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `parseBillingError(error)`       | `(error) => Error`                  | Envuelve una respuesta de `402` en un `BillingError`. Devuelve los demás errores sin cambios             |
| `isBillingError(error)`          | `(error) => boolean`                | Devuelve `error instanceof BillingError`                                                                                       |
| `formatBillingErrorForUI(error)` | `(error) => BillingErrorUI \| null` | Devuelve `null` para errores que no son de facturación. En caso contrario, devuelve `{ alertTitle, alertSubtitle }` para un modal de actualización |
| `isRuntimeNotReadyResponse(error)` | `(error) => boolean` | Es true para el `503` del daemon del sandbox mientras el runtime de la sesión aún no puede aceptar una solicitud. La solicitud no se reenvió, por lo que es seguro reintentar. Misma respuesta en runtime y pi |
| `isSandboxNotReadyError(error)` | `(error) => boolean` | Es true para la respuesta anterior y para un sandbox detenido, iniciándose o aparcado. Represéntalo como «despertando» y sigue consultándolo |
| `isRuntimeStartingError(error)` | `(error) => boolean` | Es true para ambas respuestas anteriores y para una URL de runtime que aún no está fijada. Un límite de errores reintenta en este caso en lugar de mostrar un fallo |

```ts
import { formatBillingErrorForUI } from '@beeuni/sdk';

try {
  await beeuni.session(projectId, sessionId).start();
} catch (err) {
  const ui = formatBillingErrorForUI(err);
  if (ui) showUpgradeModal(ui.alertTitle, ui.alertSubtitle);
}
```

<a id="in-beeunisdkreact"></a>

### En `@beeuni/sdk/react`

`@beeuni/sdk/react` vuelve a exportar `BillingError`, `RequestTooLargeError`, `parseBillingError`, `isBillingError` y `formatBillingErrorForUI`. No vuelve a exportar `ApiError` ni `AuthError`; impórtalos desde `@beeuni/sdk`.

`useSession` clasifica cada fallo de `send`, `answerQuestion`, `answerPermission` y `rejectQuestion` en un único objeto `sendError`, por lo que no necesitas escribir comprobaciones `instanceof` manualmente:

```ts
interface BeeuniSendError {
  kind: 'billing' | 'runtime-not-ready' | 'runtime-error';
  message: string;
  billing?: BillingError; // set when kind is 'billing'
  cause: unknown;
}
```

```tsx
const s = useSession(projectId, sessionId);

if (s.sendError?.kind === 'billing') {
  const ui = formatBillingErrorForUI(s.sendError.billing);
}
```

Consulta [Hooks de React](/docs/es/sdk/react) para ver el resto de `useSession`.
