Ctrl K
ComunidadGuías…

docs/governance.es.md

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

Archivo original ↗Preguntar a KiBot
---
source_sha: "b373de64c03d"
---

# Governance { #governance }

Budgets, aprobaciones, alertas y el registro de auditoría. Las cuatro cosas que
convierten una plataforma de agents en algo detrás de lo que puedes poner una
tarjeta de crédito.

Todas se aplican igual en cada superficie, porque cada superficie pasa por un
único runner.

La más reciente de esas superficies es un **trigger**: un agent que se ejecuta a
sí mismo según un horario o ante un evento entrante — una issue de GitHub, un
correo recibido (mira [Conceptos](concepts.md#trigger)).

Usa el mismo registro de gasto, cola de aprobaciones y auditoría. La
[política de acciones desatendidas](#routine-action-consent) refuerza qué llamadas
requieren aprobación; el consentimiento explícito identifica acciones y una versión.

El trabajo desatendido también define el resultado de un **rechazo**:

- Un run disparado que el budget detiene termina en `budget_exceeded` y **espera
  al siguiente disparo** en lugar de reintentarse.
- Un run disparado que quien lo creó ya no puede hacer — porque dejó la
  organización, o perdió su grant sobre el agent — **desactiva el trigger** y
  escribe una entrada de auditoría diciendo por qué.
- Un run disparado que falla en el modelo mismo — una caída del provider, una
  clave revocada — se registra como `failed` y se deja para el siguiente disparo,
  en vez de hacer que el heartbeat reintente contra el mismo muro. El
  `last_run_id` del trigger se queda apuntando a él, para que su historial siga
  siendo honesto.

Un rechazo que un heartbeat reintentara cada minuto sería una factura, o una
alerta, que no para nunca.

!!! warning "Lo que no se traga es un fallo que nunca llegó a ser un registro"

    Si el run alcanzó un estado terminal pero la escritura que lo registraba no
    hizo commit — una transcripción o una escritura del estado de la conversación
    que lanzó una excepción después de tener ya la respuesta —, el disparo **hace
    fallar el flow** en lugar de informar de un éxito. El run a medio escribir se
    revierte en vez de quedar confirmado como completo sin nada detrás.

Un trigger de evento añade un rechazo más en su borde: un webhook cuya firma no se
verifica contra el secreto propio del trigger es un 403 que nunca llega al runner.

## Entrega de rutinas { #routine-delivery }

Una rutina creada en un chat privado mediante el MCP de la plataforma responde en
ese chat, salvo que elijas otro destino. Su tarjeta muestra el estado actual, la
próxima activación y los controles existentes para editar, pausar y eliminar.
El registro de ejecuciones sigue disponible en Routines.

Antes de guardar una respuesta terminada, la plataforma vuelve a comprobar la membresía activa de la persona creadora y su permiso actual para escribir en el chat elegido. Un destino eliminado o inaccesible deja la respuesta en el registro de ejecución y muestra **Última respuesta no disponible en el chat** en la rutina. Un fallo de base de datos durante la entrega del mensaje queda aislado para no deshacer la ejecución terminada ni volver a ejecutar el agente.

El aviso de finalización enlaza el chat solo tras una entrega correcta.

`last_delivery_status` distingue entrega, solo registro, ausencia de respuesta y ejecución incompleta; los registros históricos siguen sin estado conocido hasta otra ejecución. La migración `0265_trigger_delivery_status.py` añade este estado sin inferir éxitos anteriores.

Define `ends_at` con un desplazamiento UTC para el trabajo con duración limitada y
`monthly_budget_usd` para un límite mensual más estricto. La herramienta MCP de
actualización permite quitar ambos ajustes. Una clave API ordinaria no puede
autorizar a una rutina a usar las cuentas personales de quien la emitió. Tampoco conceden esa autoridad los runs en segundo plano, las suplantaciones ni los turnos grupales o públicos reanudados.

Los barridos de cada minuto procesan los vencimientos independientemente de la siguiente activación, incluso para rutinas y seguimientos pausados. Desactivan el trabajo activo, resuelven sus bloqueos pendientes y cancelan las notificaciones aún no enviadas sin llamar a un modelo. Tras reiniciarse, el worker recupera los vencimientos; la limpieza puede esperar a un worker o a otro lote, mientras las comprobaciones previas a herramientas y entregas rechazan de inmediato el trabajo vencido.

La migración `0269_routine_end_cleanup.py` añade una marca del vencimiento procesado e índices parciales de finales pendientes. No crea rutinas, permisos ni hallazgos de reemplazo. Cambiar el final reactiva la limpieza; repetir el barrido no repite los efectos ya resueltos. El downgrade elimina solo estos datos auxiliares, conservando el final configurado y la identidad de la rutina.

Una conexión MCP observada también registra su generación de autorización. Completar un nuevo consentimiento OAuth o sustituir explícitamente el token o la dirección invalida el consentimiento de origen anterior aunque siga igual el ID de conexión. La renovación normal del token, las etiquetas y los cambios de cuenta predeterminada no reemplazan esta identidad. Un origen revisado sirve para ejecuciones posteriores; una ejecución antigua pausada no puede tomar prestado el nuevo consentimiento.

Tras la migración `0270_mcp_authorization_generation.py`, revisa explícitamente los orígenes de los seguimientos existentes (`review_sources=true`). Las políticas antiguas siguen siendo legibles, pero no adivinan la cuenta de una autorización histórica. No se leen credenciales del proveedor para resolver cuentas fuera de la generación admitida. Drena los workers antiguos y actualiza API y workers juntos. Pausa los seguimientos antes del downgrade; reanúdalos solo con código que compruebe la generación y orígenes revisados. Sustituir credenciales borra la lista de herramientas anterior hasta la siguiente comprobación correcta.

Las ejecuciones de rutinas conservan las cuentas MCP y los alias de herramientas admitidos para esa ejecución. Al reanudar una llamada se reutilizan esos alias: renombrar una cuenta o cambiar la predeterminada no redirige una aprobación existente. Los delegados permanecen dentro de las cuentas admitidas. Antes de llamar se comprueban de nuevo propietario, dirección, generación de autorización y herramientas permitidas; las cuentas nuevas o modificadas se filtran antes de resolver credenciales.

Una rutina pausada sin este estado de admisión no puede continuar llamadas MCP de cuentas gestionadas. Inicia una ejecución nueva tras revisar su acceso. No cambian los valores del agente interactivo ni las políticas explícitas de aprobación; las excepciones de aprobación por cuenta siguen siendo independientes de esta comprobación del origen.

La política de versiones permite seguir la versión publicada predeterminada o fijar una versión inmutable. Una publicación no cambia la versión fijada. Elimina la fijación para volver a seguir la predeterminada. Un entorno con nombre y una versión fijada se excluyen mutuamente; siguen vigentes los permisos, presupuestos y aprobaciones actuales.

El formulario de la rutina permite definir o quitar su fecha de fin. La fecha usa la zona horaria del dispositivo indicada junto al campo, independientemente de la zona del cron, y se envía a la API como un instante con desplazamiento UTC. Editar otro campo conserva el fin existente.

Una rutina vuelve a comprobar su creador, membresía activa, rol y permisos de ejecución antes de cada herramienta, también en delegaciones y después de una aprobación. Pausar, borrar, caducar, retirar el consentimiento personal o cambiar esa autoridad impide las siguientes llamadas. La comprobación no revierte efectos ya enviados a servicios externos. La migración `0260_routine_run_origin` conserva el origen tras borrar la rutina; los horarios antiguos aparcados cuyo origen ya se perdió se rechazan en lugar de usar la autoridad de quien aprueba.

## Budgets { #budgets }

!!! abstract "Tres niveles, y no son variaciones de un mismo número"

    El tope de un agent medido contra el total de la organización lo agotan los
    runs de sus vecinos; el de la organización medido contra un solo agent no es
    techo alguno. Mira [por qué no se pueden fundir](#why-they-cannot-be-collapsed).

| Nivel | Se fija en | Mide | Lo sube |
| --- | --- | --- | --- |
| **Agent, mensual** | el spec del agent | los runs de ese mismo agent | quien pueda editar el agent |
| **Departamento, mensual** | los ajustes del departamento | los runs que iniciaron sus miembros, en cualquier agent | quien tenga `members:manage` y `budgets:manage` |
| **Organización, mensual** | los ajustes de la organización | cada run _y_ cada ingesta de la organización | quien tenga `budgets:manage` |

Una **organización nueva empieza con el techo de la organización ya puesto** — el
`DEFAULT_ORG_MONTHLY_BUDGET_USD` del deployment ([configuración](configuration.md),
$100 de serie) —, así que un tenant recién creado no está a un agent desbocado de
una factura sorpresa. A partir de ahí es un tope corriente: editable en la fila de
la organización, y aplicado exactamente igual que uno puesto a mano. Un deployment
que prefiera arrancar sin tope deja ese ajuste vacío; en cualquier caso, el tope de
una organización existente nunca se cambia por ello.

### Por qué no se pueden fundir { #why-they-cannot-be-collapsed }

Antes lo estaban, con `min()`, y el resultado era erróneo. El tope de un agent
medido contra el total de la organización lo agotan los runs de sus vecinos, que
es precisamente lo que hace que no sea un tope. El tope de una organización medido
contra el gasto de un solo agent no ataría nunca.

Así que cada tope mide su propia magnitud, y la consulta viaja con el límite. Un
agent de $5 bajo un techo de $50 ata ahora cuando _él_ ha gastado $5, y el rechazo
nombra el tope que de verdad ató en vez de deducirlo de cuál de los dos números era
menor.

Un agent sigue sin poder aflojar el techo de la organización: la entrada de la
organización está presente con su propio número pida lo que pida el spec, y el
gasto de un agent forma parte del de la organización — así que un agent de $100
bajo una organización de $10 se detiene en $10.

Ambos topes se leen donde está su gasto: el de la organización en su propia fila
(`GET /orgs/{org_id}`), y el de cada agent como `budget_monthly_usd` en el listado
de agents — el número de la versión _publicada_, ya que es la que aplica el runner,
no lo que prometa el borrador en este momento. La tarjeta de margen del dashboard
cruza estos datos con `GET /spend`, de modo que se puede ver un tope acercándose
antes de que `budget_exceeded` empiece a aparecer en el historial de runs.

### El tope de un departamento { #a-departments-cap }

Un [departamento](departments.md#a-departments-budget) puede llevar su propio
tope mensual. Se sitúa entre los otros dos: un run se comprueba contra el tope
del agent, luego contra cada departamento con tope de la persona que lo inició y
por último contra el de la organización. Cada departamento mide el mes de sus
propios miembros, así que a una persona de Finanzas y Ventas la detiene el que
se agote primero, y el rechazo lo nombra - _Sales department monthly budget
exhausted_.

El responsable del departamento y los administradores reciben un aviso cuando
supera el 80% de su mes, y otro cuando su tope detiene un run. Un run que no
inició nadie - una programación, un visitante en un canal - no pertenece a
ningún departamento y no se cuenta.

### Se aplica antes de la petición { #enforcement-is-before-the-request }

!!! danger "Antes, no después"

    Comprobarlo después significa que la petición que rompió el budget ya estaba
    pagada — y un bucle puede pasarse por una llamada cara cada vez.

!!! important "Un run fallido registra igualmente lo que gastó"

    Un budget que ignora los fallos no es un budget. La contabilidad ocurre en un
    bloque `finally` en cada superficie, y el commit es explícito en lugar de
    dejarse al contexto de sesión — que revierte ante cualquier excepción y no se
    alcanza en absoluto si hay una cancelación.

!!! important "Una línea base es lo que han gastado los _demás_ runs"

    Ambas consultas dejan fuera la fila del propio run que pregunta. Un run
    reanudado conserva su fila, y para entonces `finish_run` ya ha confirmado lo
    que gastó — mientras el ledger se vuelve a sembrar con la misma cifra, cosa
    que tiene que pasar, o terminar la continuación sobrescribiría el coste con lo
    que costó solo la continuación. Contado en ambos sitios, un agent con tope de
    $10 que había gastado $6 y se aparcó volvió con `6 + 6 = 12` en su primera
    petición al modelo y fue rechazado con $4 de margen, diciéndole a su dueño que
    había alcanzado un tope del que estaba al 60%. El tope de la organización se
    contaba doble igual.

La guarda es una parada en seco para un run que _ve_ el gasto, y el coste propio de
un run solo aterriza en su fila cuando termina.

Así que la línea base que lee un run es la suma de los runs que ya han terminado, y
**los runs concurrentes son invisibles entre sí**. Cincuenta runs que arrancan
juntos contra una organización a una llamada de su tope leen todos la misma línea
base por debajo del tope, todos siguen adelante, y se pasan hasta por su coste
combinado.

Eso es una propiedad de un agregado sin una sola fila que bloquear — a diferencia
del exceso por run y por bucle de más arriba, que la comprobación previa a la
petición sí acota. Es la razón de que el tope sea un techo sobre el gasto
**confirmado** y no una puerta que serialice runs simultáneos.

Un deployment que necesite un tope estricto ejecuta sus agents por una sola cola en
vez de en paralelo.

### Los evals gastan de los mismos budgets { #evals-spend-from-the-same-budgets }

Un [eval run](evals.md) está hecho de runs normales, así que cada caso se
comprueba contra los límites del agent y de la organización antes de cada
petición al modelo, y lo que gasta cuenta contra ambos: no hay un budget aparte
para evals. Un dataset grande puede por tanto agotar el budget del mes que
necesita el tráfico de producción. Iniciar un run muestra lo que conserva cada
límite una vez gastada su estimación, y se rechaza antes de que nada se ejecute
cuando la estimación supera lo que queda en cualquiera de los dos; consulta
[El budget que deja](evals.md#the-budget-it-leaves).

### Un run cuesta más que sus peticiones al modelo { #a-run-costs-more-than-its-model-requests }

Una búsqueda en la base de conocimiento embebe la pregunta antes de poder
buscarla, y ese embedding se factura al run que lo pidió. El servicio de
embeddings es global al proceso — sirve a cada run y a cada trabajo de ingesta a la
vez — así que apunta contra el run que esté _medido en ese momento_ en lugar de
recibir un budget como argumento.

Lo que convierte al medidor en algo que una superficie puede olvidar, y olvidarlo
es silencioso: ninguna excepción y ningún aviso, solo un run que informa de menos
de lo que gastó y un mes de la organización que nunca lo ve. Por eso el medidor
pertenece al run preparado y no a la superficie. Abrir uno no es un paso que una
superficie nueva tenga que conocer, porque no hay forma de ejecutar un agent
preparado sin él.

La capability [Conversaciones largas](reference/capabilities.md#context-management) es el otro
caso. Su estrategia de resumen escribe el resumen a través de un agent que
construye ella misma, así que esa petición no pasa por ninguna guarda de budget; la
capability apunta lo que costó contra el mismo medidor. Estar _fuera_ de la guarda
tiene una consecuencia que conviene conocer: el gasto se registra en vez de
rechazarse, así que una compactación que cruza un tope detiene el run en la
petición siguiente.

### Un coste que no se pudo medir lo dice { #a-cost-that-could-not-be-measured-says-so }

El coste facturado por Stav prevalece sobre el catálogo: `usage.cost` con
`cost_currency`, o las cabeceras `x-stav-cost` y `x-stav-cost-currency`. Incluye
Chat Completions y Responses, también con streaming. USD se usa directamente;
NOK requiere `STAV_NOK_PER_USD` ([configuración](configuration.md)), mantenido
por el operador. Indica NOK por dólar: `10` dividiría NOK entre 10; no es una
recomendación de tipo actual. No se consulta ningún tipo automáticamente.
Usage conserva el importe, la moneda, el tipo y el coste en USD; el ledger
marca la fuente como informada por el proveedor. Los importes inválidos, las
monedas no admitidas o la conversión ausente mantienen el catálogo como respaldo.

Sin un coste utilizable informado por el proveedor, se usa `genai-prices`.
El paquete no conoce todos los modelos. Cuando un run llega a uno del que no
tiene entrada, esa petición se apunta a cero y el run se marca con
`cost_is_partial` — el total se queda corto exactamente por lo que costaron esas
peticiones, y la lectura honesta es la de un **suelo**.

Esa marca viaja ahora hasta el final. Está en la fila del run, en la fila del
mensaje que escribe un turno, y en el total que informa una conversación; cada
superficie que dibuja dinero dibuja un `≥` delante en vez de una cifra que se lea
como exacta. Un nulo en un mensaje escrito antes de que existiera la columna
significa _no registrado_, que no es la misma afirmación que "exacto" — un cliente
solo marca lo que sabe.

**Ahora lo registra cada superficie, y cada turno registra su propia parte.** Un
mensaje escrito por un canal, la API o el widget no llevaba coste alguno hasta hace
poco, así que un hilo de Slack no se podía totalizar. El número que se escribe es
la _diferencia_ respecto a lo que ya reclaman los turnos anteriores de ese run, no
la cifra de la fila del run: una fila de run es acumulativa, y un run que se aparcó
y fue reanudado escribe dos turnos de asistente — estampar ambos con la fila
contaría dos veces la mitad aparcada. Los mensajes de un run suman por tanto
exactamente lo que el run dice que gastó.

**Un turno que el run interrumpió a medias lo dice.** Un run cancelado deja lo que
el agent hubiera escrito cuando se cerró el socket o se pulsó `stop`, y eso se lee
igual que una respuesta terminada — así que quien lo lee toma una respuesta truncada
por todo lo que el agent tenía que decir, y el dinero gastado parece haber comprado
eso. La transcripción lleva el estado del run por turno, y el chat lo marca.

### Cómo de lleno está el context window { #how-full-the-context-window-is }

El tercer techo, y el que nadie ve venir. Un budget rechaza con un mensaje sobre el
que alguien puede actuar. Un workspace rechaza una escritura. Un **context window**
lo rechaza el provider, a media respuesta, y el run simplemente falla.

Por eso cada agent lleva un indicador — no solo el que tenga enlazada la
capability [Conversaciones largas](reference/capabilities.md#context-management), porque el aviso
importa más en el agent que _no_ va a compactar. Informa de cuántos tokens llevaba
la última petición de un turno, _después_ de cualquier compactación: la lectura baja
cuando la compactación funciona, porque mide lo que salió y no lo que guarda la
conversación.

El número es el `input_tokens` del propio provider, no una estimación del historial.
Un recuento de caracteres no puede ver las definiciones de las herramientas, y esas
se facturan en cada petición — miles de tokens en un agent con conocimiento, una
sandbox y delegación, que es un tercio de la cifra real faltando justo en el momento
en que la cifra importa.

**El recuento se guarda en el turno; la proporción no.**

Cuánto historial hay sobrevive a un cambio de modelo. Qué fracción de una ventana
es eso, no — y el chat permite cambiar de modelo entre turnos.

Un historial de 500.000 tokens es la mitad de un modelo con contexto de 1M y el
**390%** de uno de 128K, y lo segundo es una petición que el provider rechaza de
plano. Una proporción congelada con la lectura seguiría diciendo "50%".

Así que el denominador se resuelve allí donde se conoce la selección, a partir de
la ventana registrada en el perfil del modelo y del registro de precios detrás de
él. Donde ninguno puede decirlo, **no se dibuja proporción alguna** — un porcentaje
contra una ventana supuesta es una conjetura presentada como una medida.

Ese cambio es también para lo que sirve la compactación. Su disparador es una
**fracción resuelta por petición** contra el modelo al que va la petición, así que
un historial que cabía cómodamente en la ventana antigua se compacta en el turno
siguiente bajo la nueva — antes de que salga la petición, no después de que el
provider la haya rechazado. Un agent sin compactación enlazada tiene el indicador en
su lugar, y nada más.

**El disparador mide lo que midió el provider.** Se ancla en la respuesta más
reciente que lleve uso del provider — los `input_tokens` de esa petición contaron
las instrucciones, cada esquema de herramienta y cada mensaje anterior — y estima
solo lo que vino después. Por eso una conversación reproducida lleva lo que costó
cada respuesta: sin un ancla el disparador cuenta caracteres, y un agent real de
aquí leyó 9 tokens donde el provider había cobrado por 3.859. El indicador decía
77%; el disparador no vio nada que hacer.

**Un resumen se conserva.** La compactación reescribe los mensajes de un run; el
hilo entre turnos se reconstruye desde la transcripción, así que antes un resumen se
tiraba en la frontera del turno y el turno siguiente compraba otro sobre un
historial un turno más largo — dos turnos consecutivos de una conversación real de
aquí pagaron cada uno por un resumen de los mismos cinco mensajes, y el segundo se
anunció resumiendo nueve. Por eso el historial compactado se escribe en la
conversación, junto con hasta dónde llega, y el turno siguiente parte de él y
reproduce solo lo dicho desde entonces. Reabrir el hilo encuentra lo mismo que el
modelo.

Solo se conserva un resumen. Descartar los mensajes más antiguos y limpiar los
resultados de herramientas no cuesta nada rehacerlo, y escribirlo haría permanente
una pérdida que hoy se reconsidera contra la ventana en cada turno.

Un ajuste no puede funcionar, y lo dice en vez de ejecutarse. Cuando las
instrucciones y los esquemas de herramientas por sí solos ya pasan del disparador,
ningún resumen puede bajar de él — no están en el historial que se resume. Cada
petición compraría entonces un resumen que no cambia nada. La compactación se omite,
el chat dice por qué, y el arreglo es de quien escribe el agent: una ventana mayor,
o una fracción más alta.

Ese rechazo se apoya en un número que produce una _respuesta_, así que un turno no
puede medir el suyo antes de tener que decidir — y un turno de chat suele ser una
petición. La conversación lleva la última lectura, y un run parte de ella. El primer
turno de un hilo no tiene por tanto nada en que basarse y compacta según lo
configurado; desde el segundo, el rechazo está disponible. Solo se rechazan las
estrategias que compran algo: descartar los mensajes más antiguos y limpiar los
resultados de herramientas no llaman a ningún modelo, así que se ejecutan sea cual
sea la ventana.

### La delegación gasta el budget del padre { #delegation-spends-the-parents-budget }

Un run puede contener la conversación entera de otro agent — mira
[delegado frente a especialista inline](concepts.md#delegate-vs-inline-specialist).
Un run tiene **un solo ledger de gasto**, y cada delegado escribe en él. Eso es lo
que hace que el tope del padre vea el gasto de una delegación antes de su siguiente
petición al modelo, justo en el momento en que la delegación multiplica lo que
puede costar un turno. Cada entrada lleva estampada la delegación que la apuntó,
que es como un único ledger sigue respondiendo a "cuánto costó _este_ delegado" —
mira más abajo.

De ahí se sigue que **los topes que atan dentro de una delegación son los del
padre**. El `budget.monthly_usd` propio de un delegado no se aplica a mitad del run
del padre: dos guardas midiendo un mismo ledger contarían doble cada petición, y el
techo que importa es el del run que alguien inició. El tope propio del delegado
sigue gobernando los runs _del delegado mismo_.

Cada delegado, eso sí, pone precio a sus propias peticiones, porque una guarda pone
precio a lo que registra: un delegado en Anthropic medido por una guarda construida
para OpenAI se tarifaría contra el catálogo equivocado — en silencio, y normalmente
como no tarifado.

Existen tres techos más porque un budget es mala forma de parar un fan-out: solo se
entera cuando el dinero ya se ha ido. `max_depth` acota el anidamiento, `max_fanout`
acota cuántas delegaciones corren a la vez, y el `max_steps` propio de cada delegado
acota su bucle. Mira la
[capability `subagents`](reference/capabilities.md#delegation).

Dos de esos tres son del propio delegado, que es la línea que el budget no cruza:
`max_steps` se lee del spec del delegado, y su `max_depth` limita cuán profundo
puede ir _él_ por mucho margen que le quedara a quien lo llamó. Un tope sobre el
gasto es un tope sobre el run que alguien inició; un tope sobre el anidamiento es
una decisión que tomó quien escribió el delegado y que leen sus revisores, así que
quien llama no puede ensancharla.

### Cómo se registra un run delegado { #what-a-delegated-run-is-recorded-as }

Una delegación a un agent **publicado** obtiene su propia fila en `agent_runs`, con
`parent_run_id` y el id de tarea de la delegación. Un **especialista inline** no
obtiene ninguna: no tiene un agent al que atribuirla, así que su coste es el _del
run_ y la llamada a la herramienta en la transcripción es el registro.

Cuál es ese run, sin embargo, es la pregunta que respondió la
[#228](https://github.com/vstorm-co/agenticos/issues/228).

- Un especialista directamente bajo el agent propio del run se factura a la **fila
  de nivel superior**, que de todos modos es el ledger entero.
- Un especialista bajo un **delegado publicado** se factura a la fila _de ese
  delegado_, no a la de nivel superior — así el mes del delegado incluye lo que
  gastó su especialista, que es el único sitio donde podría aterrizar honestamente.

Cada entrada del ledger lleva por tanto dos atribuciones: la delegación que la hizo,
para el panel, y la fila de agent más cercana a la que se factura, para el mes.

Las dos coinciden en cada petición que un delegado publicado hace por su cuenta, y
divergen solo bajo un especialista inline — cuyo panel conserva su propia parte
mientras su gasto llega a la fila de su ancestro.

!!! important "La fila del padre es la autoridad; la de un hijo es su parte de ella"

    Un delegado gasta contra el ledger compartido, y **cada entrada de ese ledger
    lleva la delegación que la hizo**. El coste de una delegación es la suma de sus
    propias entradas — las peticiones que emitió su propio agent, tarifadas una
    vez, por la misma consulta que usa el total del run. Es exacto en ambos modos y
    a cualquier profundidad, y no depende de cuándo se liquidara la delegación.

    Antes dependía exactamente de eso, y eran dos defectos. El número era el
    *crecimiento* del total compartido a lo largo de la delegación, así que una
    delegación en segundo plano — liquidada cuando se vuelve a consultar, que puede
    ser después de que el padre haya respondido — absorbía todo lo que el padre
    gastara entretanto: un delegado que gastó $0,01 quedaba registrado en $0,51 si
    el padre gastaba luego $0,50. Y un delegado que delega a su vez tenía el gasto
    de sus propios delegados dentro de su ventana, que las filas de estos registran
    otra vez, así que su total mensual contaba a sus nietos.

    Partirlo con un ledger *por agent* sigue siendo el diseño a evitar — es lo que
    impide que el tope del padre ate en absoluto. Un solo ledger, atribuido,
    conserva ambas propiedades: el tope del padre ve cada petición antes de la
    siguiente, y cada fila delegada dice lo que gastó ese único agent, sus propios
    especialistas inline incluidos y sus delegados publicados excluidos.

    La fila del padre sigue siendo la autoridad para el run. Su `cost_usd` es el
    ledger entero, delegados incluidos, que es lo que se le factura a la
    organización; las filas hijas dividen ese mismo dinero por agent y nunca le
    suman. `cost_is_partial` es también por fila: un padre sobre un modelo que
    `genai-prices` no conoce convierte el total del padre en un suelo, y no dice
    nada sobre un delegado que corrió sobre uno tarifado.

    El `started_at` y el `ended_at` de una fila delegada son el lapso **propio** de
    la delegación, leído del manejador de tarea que la librería estampa cuando el
    delegado empieza y cuando acaba — no el momento en que se liquidó la fila.
    Desde la liquidación, una delegación en segundo plano se leía como un run de
    duración cero a la hora equivocada, ordenado después de trabajo que terminó
    antes que ella; dos que de verdad se solapaban quedaban registradas en el mismo
    instante sin nada que lo dijera. Un manejador terminal con final pero sin
    inicio — un delegado cancelado o fallido antes de empezar a ejecutarse —
    registra un lapso de longitud cero en ese final, nunca un nulo; y donde la
    librería rechaza antes de que exista siquiera un manejador — un `chat_trace_id`
    desconocido — no se escribe fila delegada alguna. Una delegación que se aparcó
    en una aprobación abarca cada turno en el que corrió: su inicio más temprano se
    arrastra a través del aparcamiento igual que su coste (abajo), así que la fila
    empieza cuando el delegado empezó por primera vez y acaba cuando por fin acabó
    — no en la reanudación que la liquidó. Los dos no se suman como sí se suma el
    coste; la respuesta honesta es el inicio del primer segmento y el final del
    último.

**Una delegación que se aparcó en una aprobación es más de una parte.** Sus turnos
corrieron en procesos distintos contra ledgers distintos, y el ledger de un turno
reanudado es un objeto nuevo que no guarda nada de antes del aparcamiento — así que
lo que registra la fila hija es cada segmento sumado: el estado aparcado conserva lo
que la delegación había costado al detenerse, y el turno que la termina añade su
parte. Se escribe una fila, una vez, en el turno donde la delegación acaba — un
delegado que se aparcó dos veces deja tres segmentos y una fila.

`cost_is_partial` se arrastra igual, y por un motivo que el dinero no comparte:
ahora es por fila y no por run, así que un delegado que hizo una petición no
tarifada _antes_ de la aprobación y se reanudó sobre un modelo tarifado tendría si
no una fila afirmando un coste exacto. La marca es verdadera si lo fue en cualquier
segmento.

Vale la pena decirlo porque el fallo al que sustituye era invisible. La fila
guardaba solo lo que el delegado gastó _después_ de la última reanudación, que en la
forma corriente — haz el trabajo, luego pide permiso para actuar sobre el resultado
— es la mitad pequeña. Nada dejaba de cuadrar, porque el dinero estaba en la fila
del padre desde el principio; lo que estaba mal era cada número que responde a
"cuánto costó este delegado".

La fila hija es lo que hace respondibles dos preguntas distintas, y quieren la
aritmética contraria:

| La pregunta | Filas hijas |
| --- | --- |
| **¿Qué debe la organización?** | **excluidas** — la fila del padre ya contiene esos tokens, así que contar ambas le factura a la organización dos veces una misma petición |
| **¿Cuánto costó _este agent_ este mes?** | **incluidas** — las filas de un delegado son el único sitio donde se registra su propio gasto, y cada una guarda las peticiones de ese agent y las de sus especialistas inline ([#228](https://github.com/vstorm-co/agenticos/issues/228)) pero no las de sus delegados publicados, que tienen filas propias |

La segunda es lo que hace respondible "el investigador costó $40 este mes", y es
sobre lo que se dispara un informe de uso por agent o una alerta de budget sobre ese
agent. El número mensual de la organización lleva además el gasto de ingesta, que el
número por agent no lleva: indexar una base de conocimiento compartida no es gasto
del agent de nadie.

Una fila hija fallida se somete también a la regla del padre sobre lo que puede
decir `error`. Lo que se lanza bajo un delegado es un cliente de modelo cuyo mensaje
puede llevar la URL de la petición fallida — clave incluida, en un endpoint propio —
así que la fila y el frame de cierre de la delegación guardan la misma frase
controlada que guarda la fila del padre, y el texto del provider va al log del
servidor. Un delegado detenido por su límite de uso o por un techo de budget
conserva entero el mensaje del límite: eso es un techo haciendo su trabajo, no un
fallo que diagnosticar.

La regla alcanza también la transcripción del _padre_. Cuando un agent delega en
segundo plano consulta `check_task` y `wait_tasks` por el resultado, y lo que esos
responden se convierte en una fila de llamada a herramienta en la conversación —
guardada entera, porque el retorno de una herramienta es la respuesta de la propia
herramienta y no algo que compusiera esta plataforma. Así que nombran la clase de la
excepción en lugar de repetir el mensaje del provider, que es
`subagents-pydantic-ai` 0.2.20 y la razón de que el suelo esté ahí.

**La cifra por ventana del dashboard también lo lleva.** `GET /stats/usage` responde
con un bloque `cost` para el periodo que eligiera el filtro, y ese bloque son los
runs _más_ la ingesta — la misma aritmética con la que se mide el tope mensual — con
`model_usd` e `ingestion_usd` al lado para que quien lo lea vea adónde fue el dinero
sin restar. Informaba solo de la mitad de modelo hasta la 0.0.152, lo que ponía dos
definiciones distintas de coste en una tarjeta: el titular se movía con el filtro de
periodo y contaba runs, mientras la línea de mes en curso debajo contaba la factura
entera, y nada decía que respondieran a preguntas distintas. En un deployment que
indexa documentos simplemente discrepaban.

Con `scope=own` la mitad de ingesta es cero y no una parte proporcional: un documento
lo indexa un worker y `ingestion_spend` no registra usuario, así que cargarle la
ventana de una persona por una colección que sincronizó otra sería inventarle gasto.

**Cada consulta tiene que decir a cuál de las dos responde**, y la primera columna es
la predeterminada. La cifra de mes en curso y el desglose por agent detrás de ella
excluyen las filas hijas, así que suman al total impreso encima — y el correo de uso
de la organización informa de ese mismo total y no de una suma de uno de ellos. Solo
una pregunta hecha _sobre un agent_ las incluye. Tres de estas cinco consultas se
entregaron sin la distinción y cada una informaba de $1,40 por $1,00 de trabajo; si
se añade otra, el valor por defecto es el seguro.

#### Las dos preguntas sobre el proveedor necesitan una tercera respuesta { #the-two-vendor-questions-need-a-third-answer }

**By provider** y **By key** no pueden usar ninguna de las dos columnas, y
equivocarse ahí es invisible en pantalla. Excluir las filas hijas totaliza bien y
luego atribuye el dinero del delegado al proveedor del _padre_, porque ese es el
provider de la fila que se suma: un orquestador en OpenAI que delegaba $0,40 de
trabajo a un agent en Anthropic informaba `openai $1.00`y ninguna fila de Anthropic.
Incluirlas informaba`openai $1.00` + `anthropic $0.40` — más que la factura.

Así que esas dos suman el gasto **propio** de cada run: su coste menos el de sus
delegaciones directas. `openai $0.60` + `anthropic $0.40`, que es a la vez la
atribución correcta y el total correcto. Anida — un delegado que delegó a su vez
tiene a sus nietos descontados por él, una vez — y sumado sobre cada fila sigue
dando la factura, porque el coste de cada hijo lo añade su propia fila y lo quita la
de su padre. Una clave funciona igual, e importa más: una clave es lo que alguien
rota cuando una factura parece mal.

### El budget de una llamada de voz { #a-voice-calls-budget }

Una [llamada de voz](voice.md) es un run, y su gasto cuenta contra los budgets
del agent y de la organización como cualquier otro. Tres cosas cambian, porque
una llamada no se puede comprobar antes de cada petición como un run de texto:

- **Antes de empezar**, la duración máxima posible de la llamada multiplicada por
  el precio por minuto del peor caso del modelo se comprueba contra el budget
  restante. Una llamada que no se podría pagar se rechaza, en vez de empezar y
  cortarse a medias.
- **Mientras dura**, cada respuesta se anota en el run cuando termina, y el gasto
  del mes se vuelve a leer cada 15 segundos, así que se ve lo que otros runs
  gastan entretanto. Un budget que se agota a mitad de llamada recibe una frase
  hablada y termina la llamada, registrada como `budget_exceeded`.
- **Su duración es la del agent.** Una llamada dura como mucho la llamada más
  larga del agent, 10 minutos por defecto y 50 como máximo; no hay una bolsa
  aparte de minutos de llamada, así que los budgets son el único límite de lo
  que gastan las llamadas.

Una llamada cuyo proceso murió la cierra el barrido de voz con el coste que
registró su último heartbeat, a diferencia de un run de texto, cuyo gasto muere
con su proceso.

### Trabajo por iniciativa propia { #proactive-work }

Los runs nocturnos de sugerencias y briefing se registran bajo el agent inicial **AgenticOS** publicado de
la organización (slug `agenticos`) para que cada uno tenga una fila, un coste y un
rastro, pero el agent inicial no se ejecuta ni elige el modelo: cada uno de esos
runs lleva el [rol de sistema](#system-agents) que hizo el trabajo. El modelo de
proactividad aparte y los límites se eligen en **Organizaciones → Miembros →
Proactividad**.

Actividad muestra el icono y el filtro **Proactivo**, las
peticiones al modelo y sus costes, y el intercambio registrado, con los
resultados de herramientas y la respuesta estructurada final. Las comprobaciones
fallidas conservan el intercambio disponible antes del fallo. Estos registros
requieren `runs:view`, como otros registros de Actividad, y no crean
conversaciones en el Chat. Los runs antiguos no pueden recuperar intercambios que
nunca se registraron; los registros demasiado grandes dicen expresamente que se
recortaron.

El modelo independiente y los límites se eligen en **Organizaciones →
Miembros → Proactividad**. Activity muestra el icono y filtro **Proactivo**, las
solicitudes al modelo y sus costes, y el intercambio registrado con resultados
de herramientas y respuesta estructurada final. Las comprobaciones fallidas
conservan el intercambio disponible antes del error. Como los demás registros
de Activity, requieren `runs:view` y no crean conversaciones en Chat. No se
pueden recuperar intercambios antiguos que nunca se registraron; los registros
demasiado grandes indican explícitamente que se recortaron.

El campo del modelo usa el mismo selector de proveedor, modelo y clave del
Vault que el Builder de agentes. Con `connections:manage`, una persona puede
crear un perfil de modelo allí; sin ese permiso, elige un perfil existente.
El perfil nuevo se guarda inmediatamente. **Guardar** aplica la selección a las
sugerencias nocturnas. El briefing enumera hallazgos existentes sin llamar al modelo. Las vigilancias del agente elegido comparten
el límite por persona.

La plataforma puede hacer un poco por iniciativa propia, y no hace nada de eso
hasta que una organización lo activa: la tarjeta **Proactividad** en su página de
miembros (`budgets:manage`). Activada, una tarea en segundo plano lee las
peticiones recientes de cada persona los días y a la hora que eligió la
organización - en la zona horaria de cada persona, o una hora antes del horario
laboral que indica su perfil - y propone una rutina para una petición que repite,
un perfil o los primeros mensajes que ofrece su chat vacío
([Consola](console.md#chat)).

Cada uno es una llamada al modelo, con el modelo
que la organización eligió al activarla (no hay uno por defecto:
`proactive_model_profile_id` es obligatorio), registrada en el historial de runs
como un run de la superficie **proactive** y contada contra el budget de la
organización como cualquier otra. Una persona puede excluirse en **Ajustes →
Asistente**.

La mirada nocturna vence 20 horas después de su propio último intento. Las
vigilancias y el mantenimiento de memoria no cambian ese momento. Las
vigilancias siguen contando para el límite personal de gasto en proactividad.

Una mirada completada guarda la persona y un hash de cada prompt leído, sin
copiar el texto. Finalizar una pausa de aprobación o pregunta ya leída no
provoca otra llamada de pago al modelo. Un prompt aplazado sigue sin leer
hasta que una mirada posterior lo incluya; el texto modificado vuelve a ser
nuevo. Las llamadas fallidas al modelo no marcan prompts como leídos.

Tras actualizar, los runs antiguos de fondo sin rol no aplazan la siguiente
mirada. Los mensajes existentes no tienen comprobante de lectura, por lo que
los prompts recientes elegibles pueden incluirse una vez para ponerse al día,
con los interruptores y presupuestos habituales.

La tarea nocturna solo lee un prompt que pasó el
[guardrail](reference/capabilities.md#guardrails) de entrada del agent al que se
envió, porque el modelo de proactividad no tiene guardrails propios:

- Un prompt que un guardrail rechazó queda fuera.
- Un prompt que ningún run tomó queda fuera: uno enviado a un agent archivado,
  uno rechazado antes de abrirse su run y un prompt del chat que pasa a un
  workflow.
- Un prompt cuyo run sigue en marcha espera. La tarea lo lee una noche posterior,
  cuando el run ya ha terminado.
- Si un run se detuvo antes de su primera petición al modelo, por ejemplo por un
  budget, la tarea pasa el guardrail de entrada de ese run por el prompt y
  deja fuera lo que bloquea. Si la versión del agent ya no existe, el prompt queda
  fuera.
- Si la retención borró el run, la tarea lee el prompt solo cuando le sigue una
  respuesta de un modelo. Un prompt anterior a la versión 1.36.0 se lee igual, así
  que el rechazo de un guardrail de entrada, que no dejó respuesta, queda fuera.
  Un turno hablado no registra el modelo, así que queda fuera cuando ya no existe
  su run.

Los prompts de rutinas se comprueban con la versión fijada del agent, la versión
del entorno indicado o la versión actual del agent, como al ejecutar la rutina.
Se omiten los destinos ausentes y las specs ilegibles, así como una rutina cuya
última ejecución fue rechazada. También se omite una rutina ya ejecutada cuyo
último run eliminó la retención: un resultado desconocido no puede anular un rechazo.

Un prompt que un guardrail redactó pasa, y la tarea lo lee tal como está guardado,
sin redactar ([#2655](https://github.com/vstorm-co/agenticos/issues/2655)).

Además, los runs por iniciativa propia de cada persona en un mes natural los
limita el **límite de trabajo por iniciativa propia** de la organización - 1 $ por
defecto, que se fija en la misma tarjeta (`proactive_limit_usd`). Una persona a la
que no le pasó nada nuevo no cuesta nada: la tarea solo llama al modelo cuando hay
algo que leer.

**Las vigilancias** ejecutan el agente publicado que elige la persona,
con el mismo motor que el chat y las rutinas, sus propias instrucciones y modelo.
Se activan por separado (`proactive_watches_enabled`) y comparten el límite de
gasto proactivo por persona, no el modelo del briefing. Créala en **Rutinas**,
junto a los horarios del agente con **Vigilar e informar**, junto a una cuenta
personal o desde un chat privado. Elige agente, cuenta, intervalo y final
opcional. Puede seguir publicaciones, un entorno con nombre o una versión
inmutable. La organización conserva el intervalo mínimo
(`proactive_watch_min_minutes`).

La publicación ya debe vincular ese servicio personal del catálogo. La vigilancia
no activa vínculos ausentes ni sustituye al agente. Aquí no se admiten servidores
propios sin un vínculo personal del catálogo. Crear aprueba esa cuenta y su
endpoint concretos, la intersección de las listas de herramientas de cuenta y
agente, y las bases de conocimiento y archivos de contexto actualmente adjuntos.
Las publicaciones posteriores pueden quitar acceso, pero no añadir cuentas,
herramientas o fuentes. **Volver a aprobar las fuentes actuales** renueva
explícitamente el permiso. Una cuenta principal nueva no sustituye la aprobada.

Cada revisión es un run **proactive** normal atribuido al agente y versión reales.
Las herramientas de origen deben tener `readOnlyHint`, y una lectura ante la que
pregunta la vinculación MCP del agent queda fuera de la revisión. Se permiten la cuenta
aprobada, conocimiento/contexto, guardrails, planificación y recordatorios del
sistema; no están disponibles web, sandbox, skills ejecutables, escritura de
memoria ni delegación. Las aprobaciones interactivas obligatorias impiden la
configuración en vez de omitirse. Se comprueban otra vez la membresía, el permiso
de ejecución, el consentimiento de fuentes, la pausa y la caducidad antes de
herramientas y de guardar resultados.

Los hallazgos nuevos conservan su identidad estable de vigilancia/elemento en la
bandeja personal y regresan al chat de origen; fuera del chat se utiliza la
conversación principal. Solo un hallazgo urgente solicitado envía además una
notificación inmediata. En el modo predeterminado **Solo cuando hay cambios
relevantes**, sin cambios no hay mensaje ni aviso de finalización.
Sin acceso al chat, el hallazgo permanece en la bandeja y se registra
`delivery_unavailable`.

El último intento se distingue de la última lectura correcta. Una respuesta del
modelo sin lectura correcta de la fuente o con un error de lectura sin resolver
es `not_completed`. Las fuentes no disponibles y el acceso revocado son explícitos;
`budget_paused` indica presupuesto agotado. Estos errores no avanzan la fecha de
éxito. Los errores inesperados siguen haciendo fallar la tarea.

`0261_watch_attempt_status.py` inicia el historial sin inventar éxitos anteriores.
`0262_watch_agent_sources.py` conserva IDs, hallazgos, intervalo y activación,
pero no adivina qué agente asignar. Las vigilancias existentes muestran
`needs_agent` y no llaman al modelo hasta que su propietario seleccione un agente
publicado y apruebe sus fuentes. Eliminar la vigilancia no borra el origen ni la
aprobación guardados en un run activo o pendiente.

Crear o modificar requiere la sesión de la propia persona o un chat humano
privado de confianza. Las claves API ordinarias, el contexto sustituto del
publicador, las rutinas en segundo plano y la suplantación no pueden conceder
este acceso permanente. Las herramientas MCP de la plataforma `list_watches`,
`create_watch`, `update_watch` y `delete_watch` usan el mismo servicio e IDs.
Las peticiones privadas completas crean o modifican inmediatamente; publicar,
invitar, eliminar y ejecutar ahora conservan sus reglas de aprobación.

Una vigilancia puede elegir **Después de cada revisión, incluso sin cambios**
(`report_mode: every_check`). Cada lectura correcta entrega el informe acordado
en el chat de origen, aunque no haya hallazgos nuevos, y utiliza el sistema
compartido de notificaciones. Un error o una lectura no disponible nunca se
presenta como ausencia de cambios. La activación permanece en la tarea; su
marcador se confirma con los resultados, de modo que reintentar una activación
completada no repite el informe ni paga otra lectura. No requiere una segunda
llamada de clasificación.

Horarios y vigilancias heredan tus preferencias de notificación.
**Notificaciones de esta rutina** permite combinar correo, push del dispositivo,
Slack, Telegram, Mattermost, Microsoft Teams y SMS, o solo la app.
`notification_channels: null` hereda; una lista vacía impide envíos externos.
La rutina no activa un canal desactivado en tus preferencias. Los envíos pendientes
revisan la política actual; pausar o eliminar la rutina los detiene. Los avisos
obligatorios de seguridad/configuración siguen siendo obligatorios. Un mensaje
aceptado por el proveedor no se puede retirar.

En **Ajustes → Notificaciones**, elige expresamente un chat privado vinculado por
plataforma para la organización actual. Vincular una cuenta no activa notificaciones.
Se excluyen grupos, cuentas ajenas y bots de otras organizaciones. Correo y push
conservan su configuración. El selector de chat en las preferencias de eventos
cambia la plataforma cuyos interruptores estás editando.

El destino queda ligado a la vinculación concreta y la autorización del bot.
Desvincular, volver a vincular o cambiar el token/servidor del bot exige elegir de
nuevo. Los envíos pendientes nunca se redirigen. Antes de enviar se comprueban
membresía, actividad, política del bot, caducidad del vínculo SMS, preferencias,
acceso al recurso y estado compartido del caso. Quien ya no puede decidir una
aprobación recibe solo el aviso de espera, sin enlace de decisión.

Las plataformas de chat no comparten un contrato de envío idempotente. Un envío
sin confirmar tiene un intento persistido: timeout, fallo del proveedor o pérdida
del worker deja un fallo visible. Un HTTP 429 de Slack que rechaza expresamente
la petición permite reintentar tras `Retry-After`, hasta cinco intentos en total.
Antes de enviar se elimina el permiso de reintento; una caída en ese intento no
lo repite. El aplazamiento invalida el token anterior de entrega. Otros errores
de chat son terminales. Correo y push conservan sus reglas de reintento.

Slack utiliza bloques de texto plano acotados y **Open in AgenticOS** al mismo
caso autorizado en consola. Nombres y resúmenes no crean menciones ni vistas
previas automáticas. Sin derecho a decidir solo aparece el aviso de espera,
sin enlace de decisión. La entrega registra aceptación por la plataforma;
el reconocimiento sigue siendo una decisión explícita del caso compartido.
La migración `0273_notification_safe_retries.py` no concede reintentos a filas
existentes. Revertirla restaura un intento por entrega de chat.

La migración `0271_notification_routes.py` añade destinos e identidades de
autorización. Drena los workers y actualiza API/workers juntos antes de elegir los
nuevos canales. Ninguna cuenta existente se activa al actualizar. El downgrade
elimina destinos, historial de entregas y preferencias de chat, y limita las
selecciones de rutinas a correo/push; no deshace envíos externos.
`0263_routine_notification_policy.py` introdujo los controles originales.

### Pausas por presupuesto { #routine-budget-pauses }

Cuando una rutina alcanza su propio límite mensual, su propietario recibe un
aviso por periodo y límite configurado. Una vigilancia indica el presupuesto
compartido de trabajo proactivo de esa persona y remite a la administración
para revisarlo. La comprobación se detiene antes de abrir una ejecución o
llamar al modelo; el aviso muestra el gasto mensual. Se respetan las
preferencias de avisos presupuestarios y los canales elegidos para la rutina.

El aviso abre un caso para esa responsabilidad y ese mes. Las comprobaciones
repetidas usan el mismo caso. Confirmar o silenciar cancela las entregas
pendientes. Cuando la siguiente activación vuelve a tener presupuesto o se
elimina la responsabilidad, sus casos presupuestarios abiertos se resuelven y
se cancelan las entregas pendientes. Cambiar el límite permite otro aviso al
alcanzarlo; un caso silenciado sigue silenciado durante ese mes.

### Consentimiento para acciones { #routine-action-consent }

Una rutina desatendida pide aprobación antes de acciones de capacidades, cambios
en archivos locales y delegaciones. Las llamadas MCP externas preguntan salvo que
su servidor las marque como solo lectura; sigue vigente una política más estricta
de la conexión. No cambian los valores del agente interactivo. El modo de código
se desactiva en rutinas para que los scripts agrupados no eviten controles por acción.

Las escrituras en Virtual Tables de AgenticOS se ejecutan dentro de sus permisos vigentes de tabla y operación sin una excepción de rutina. Una política explícita de aprobación del agente sigue aplicándose a estas escrituras internas.

El creador abre **Consentimiento para acciones** en su rutina y elige acciones de capacidades y acciones MCP individuales de una cuenta identificada. Requiere `approvals:decide` y el ajuste `chat_may_waive_approvals` de la organización. Una clave API, un agente, una sesión suplantada o un administrador actuando por otro propietario no pueden consentir. No se pueden omitir aprobaciones explícitas del agente o de una vinculación MCP.

Una excepción MCP identifica la cuenta concreta, el hash de la dirección, la generación de autorización y el nombre original de la acción. Enviar correo no autoriza borrar ni escribir en otra cuenta. Las cuentas personales solo aparecen si la rutina actúa como su creador. El selector usa la lista de herramientas guardada; comprueba la conexión o pide ayuda a su administrador cuando no esté disponible. Revocar todo el consentimiento no requiere una lista operativa.

Al guardar, la versión y configuración deben seguir coincidiendo con lo revisado
por la persona. El consentimiento identifica la versión publicada inmutable, los identificadores
estables de capacidad y herramienta y la configuración de la rutina. Cambiar el
trabajo o destino lo invalida. Cada ejecución conserva las identidades admitidas;
un consentimiento posterior no libera una ejecución suspendida y revocar y
consentir de nuevo crea una identidad nueva. Se comprueban de nuevo la pertenencia,
los permisos, la política y el consentimiento antes de cada herramienta. Cada
llamada autorizada registra una fila aprobada con persona y argumentos. Los
delegados requieren sus propias decisiones y no heredan la excepción del padre.

El consentimiento permanente también registra las revisiones actuales de las skills vinculadas, sus recursos, los archivos de contexto y las instrucciones del proyecto. Cualquier edición invalida la excepción, aunque no cambie la versión publicada del agente o se restaure después el texto anterior. Revisa las acciones de nuevo para conceder otra excepción. Las excepciones existentes sin huella de revisión siguen siendo válidas solo para agentes sin estas fuentes de instrucciones modificables.

`0267_routine_action_consents.py` no crea excepciones para rutinas existentes.
Una acción antes desatendida puede esperar ahora una aprobación. Drena los workers
antiguos y actualiza API y workers juntos antes de reanudar tareas: un worker
antiguo no aplica este control. El downgrade elimina excepciones guardadas;
permanecen los registros de aprobación y los efectos ya ejecutados.

### Respuestas de rutinas reanudadas { #resumed-routine-answers }

Una rutina terminada después de una aprobación o respuesta a una pregunta encola
su respuesta para la consola en la misma transacción que el estado final. La
entrega guarda texto final e identificadores; un reinicio no requiere otra llamada
al modelo. El estado de entrega, mensaje y aviso de finalización se guardan juntos.
Los intentos simultáneos y reintentos no añaden una segunda respuesta. Un evento
autorizado actualiza el chat de origen abierto después del commit sin reiniciarlo;
los suscriptores antiguos conservan su vocabulario de eventos.

El worker comprueba estado actual, plazo, creador y destino. La entrega al chat
revalida pertenencia activa, permiso para ejecutar el agente y escritura actual en
la conversación privada del creador. Los chats archivados están cerrados a nuevos
mensajes. Un destino inaccesible deja el resultado en Actividad y registra la
entrega no disponible. Un error de base de datos conserva el envío pendiente hasta
tres intentos; Administración → Tareas en segundo plano muestra una entrega agotada.
El aviso enlaza al chat después de la entrega correcta. La continuación en canales
externos sigue como trabajo de aceptación separado.

`0268_routine_answer_delivery.py` no reproduce resultados históricos. Actualiza
API y workers juntos. Antes de un downgrade, drena o cancela las tareas pendientes
`deliver-routine-answer`: elimina sus entregas guardadas, pero mantiene mensajes
ya entregados.

### Incidentes de ejecución { #routine-failure-incidents }

Una rutina fallida, una fuente de vigilancia inaccesible o un chat de destino
no disponible abre un caso para su propietario. Los errores continuados
actualizan la última causa y ejecución sin otro aviso. Confirmar mantiene el
caso pendiente y en silencio; silenciar esa causa sigue vigente tras la
recuperación. Un fallo nuevo después de una recuperación confirmada reabre el
mismo caso y envía un aviso nuevo.

El runner compartido informa de ejecuciones iniciales, reanudadas y cerradas
tras detectar la caída del proceso. Entre resultados terminales, solo la última
ejecución principal iniciada cambia el estado de la rutina; las delegadas y
los resultados tardíos de ejecuciones anteriores no lo sustituyen. Una vigilancia
se recupera tras una lectura verificada de la fuente; terminar el modelo no
basta. Las comprobaciones tardías conservan por separado el intento más reciente
y la última lectura correcta. Detectar la caducidad o eliminar la responsabilidad resuelve
sus bloqueos y cancela las entregas pendientes. Las rutinas pausadas, caducadas o
eliminadas no escriben una respuesta terminada en su chat de destino.

Los tiempos de observación y recuperación protegen las decisiones frente a
reintentos del callback y resultados antiguos. Un fallo al guardar el aviso
permite reintentar el incidente nuevo. Los límites del agente y de la ejecución comparten el incidente del propietario
sin otro correo de error. Los avisos de presupuesto de organización y departamento
siguen siendo alertas financieras independientes, también si el propietario es
administrador. Su incidente queda en la aplicación cuando el aviso financiero
ya le informa. Se respetan las preferencias presupuestarias.

`0266_routine_activation_clock.py` añade los tiempos sin inventar intentos
históricos. Conserva los identificadores existentes de fallos de vigilancia.
Actualice la API y los workers juntos: el worker de entrega debe comprender los
nuevos metadatos presupuestarios. Un downgrade elimina los tiempos, pero conserva
los casos, avisos y decisiones.

### Agentes de sistema { #system-agents }

Parte del trabajo es de la propia plataforma y aun así necesita un modelo. Un
**rol de sistema** le da un responsable que un operador puede ver: una
configuración por organización y rol, nunca una fila de agent, nunca una por tarea
y nunca atribuida al agent inicial ni a ningún agent cuyas instrucciones no se
ejecutaron (#2575). **Organizaciones → AI & services → Background services**
(`budgets:manage`) lista cada rol con el modelo en que corre, el gasto de este mes
y sus últimas ejecuciones.

| Rol | Qué hace | Configuración |
| --- | --- | --- |
| `daily_briefing` | La mirada nocturna a la semana de cada persona y el briefing que se construye con ella | En la pestaña Proactivity: el mismo modelo y límite, aquí solo de lectura |
| `memory_maintenance` | Lee una vez un turno admitido terminado y registra los hechos, preferencias y decisiones duraderos que contiene como [claims](reference/capabilities.md#memory-claims) | Aquí: un interruptor, un modelo y un presupuesto mensual propio |

El mantenimiento de memoria no necesita al agente inicial AgenticOS. Su run conserva el agente de origen como referencia de almacenamiento y se contabiliza como `memory_maintenance`; solo ejecuta las instrucciones de extracción de la plataforma con el modelo elegido para el rol. Eliminar o retirar la publicación del agente inicial no desactiva la extracción de otros agentes publicados.

Un rol se desactiva, no se borra. El **mantenimiento de memoria** requiere un modelo
elegido expresamente y respeta su presupuesto mensual y el tope de la organización.
Lee turnos terminados solo cuando el runner admitió a una persona real con permiso de
escritura: chats privados, mensajes de canales vinculados y sus propias tareas programadas
o de workflow. Se excluyen embeds, claves API, suplantaciones, eventos de terceros y delegaciones.

El trabajo conserva la audiencia original. Un turno privado escribe en el perfil de la
persona o su relación con el agente; un turno grupal solo en la sala de origen, aunque
el modelo considere general un hecho. Tras la respuesta del modelo se comprueban de
nuevo la membresía y los borrados bajo el bloqueo de escritura. Los trabajos antiguos
sin audiencia siguen limitados al chat web privado. El mantenimiento no comparte ni
corrige claims. La consolidación determinista no necesita modelo ni rol.

### Qué muestra el historial de runs { #what-run-history-shows }

**`GET /runs` lista solo runs de nivel superior, y su `total` cuenta esos.** El
mismo valor por defecto que la suma mensual de la organización, y por la misma
razón: intercaladas, las dos clases de fila no se pueden leer por una sola columna
de coste. Un fan-out de tres delegaciones es un run que cuesta $1,00 en la página y
$1,00 en la factura; listadas juntas eran cuatro filas de $1,00 + $0,40 + $0,40 +
$0,40 junto a una cifra de mes en curso de $1,00, y ambas mitades tenían razón sobre
una pregunta distinta.

La lista toma la misma aritmética de dos caras que las sumas de arriba, por la misma
razón — de modo que una superficie acotada a un agent muestre lo que hizo _ese
agent_, trabajo delegado incluido:

| Pregunta | Respuesta |
| --- | --- |
| `GET /runs` | Los runs que alguien inició. `parent_run_id IS NULL` |
| `GET /runs?agent_id=<id>&include_delegations=true` | El historial propio de un agent. Lo que piden el panel Recent runs del Builder y el `?agent=` de Activity, porque las filas de un delegado son el único registro de lo que hizo él mismo |
| `GET /runs?parent_run_id=<id>` | Lo que delegó ese run — la consulta para la que existe `agent_runs_parent_run_id_idx`. Tiene prioridad sobre `include_delegations` |
| `GET /runs/<id>` | Un run, delegado o no. Donde aterriza un enlace desde una transcripción |
| `GET /runs/<id>/transcript` | Los turnos de ese run, en orden — lo que una vista de detalle de run dibuja como pasos. Autorizado, no en propiedad (abajo) |

**Leer un run está autorizado, no en propiedad.**

Alguien del equipo con `runs:view` lee un run que inició otra persona. La autoridad
sobre un run es de la organización, porque un run es aquello por lo que se factura y
responde la organización — no propiedad privada de quien pulsó el botón.

Así que la decisión vive en el servicio y no en una puerta de ruta: resuelve primero
el run contra la organización de quien llama, y luego comprueba `runs:view`.

Tres consecuencias:

- Un run en **otro tenant se lee como ausente** — el mismo 404 con que responde un
  id que nunca existió, cuerpo incluido — así que la respuesta no sirve para
  descubrir que un run existe.
- Un run que corrió **sin conversación** (una llamada a la API que no pasó
  `conversation_id`) no tiene transcripción que leer, y lo dice con un
  `conversation_id` nulo en vez de una lista vacía que se leería como "no hizo nada".
- Nada de esto ensancha `GET /conversations/{id}/messages`, que sigue **acotado al
  propietario**. Que un colega pueda leer la transcripción de un run no debe hacer
  legible también el hilo privado en el que está.

**Cada turno que sirve la transcripción lleva las valoraciones que la gente dejó en
él** — el pulgar de quien lee, los me gusta y no me gusta de la organización, y el
comentario de la valoración negativa más reciente.

Una fila de mensaje sin más no guarda nada de esto, así que se leen de
`message_ratings` en un solo lote y se adjuntan a los turnos. Un turno que nadie
valoró los lleva vacíos y se lee exactamente como un mensaje corriente.

Esto es lo que permite que la vista de detalle de un run muestre las respuestas
valoradas negativamente y las palabras que las acompañaron — las conversaciones
detrás del número de calidad del dashboard (#209) — leídas donde se lee el run, y no
solo en la exportación de valoraciones del app admin.

El comentario que se muestra es el de una valoración **negativa**, nunca el de una
positiva, y el más reciente cuando un turno recibió más de una objeción.

### Qué muestran los agregados del dashboard { #what-the-dashboards-aggregates-show }

`GET /stats/usage` toma las mismas dos caras, y el mismo valor por defecto. La
respuesta compuesta es la pregunta de la organización, así que cada bloque cuenta
solo filas de nivel superior: el coste del periodo y su reparto por provider (la
doble factura de arriba), pero también el total de runs, la serie por día, el
reparto de desenlaces, las superficies, los percentiles de latencia, el número de
personas activas y la tabla por persona. Más allá del coste, una fila delegada
_copia_ el `user_id` y la `surface` de su padre, así que contarla inventaría además
una segunda persona y una segunda llegada por un canal que alguien usó una vez.

Dos agregados toman la otra cara, y ambos se preguntan sobre un agent:

| Pregunta | Filas hijas |
| --- | --- |
| `by_agent` — la tarjeta de adopción | **incluidas.** Excluidas, un agent que corre cuatrocientas veces al día como delegado de alguien no tiene fila, y la tarjeta señala como olvidado a cada agent publicado sin una y ofrece archivarlo. Sus barras pueden por tanto superar el total de runs a su lado; nada las suma |
| `?group_by=version` — la tarjeta de comparación de versiones | **incluidas.** Un especialista que solo se ejecuta como delegado no tendría si no nada que comparar entre sus versiones |

El invariante que sobrevive en cualquier caso: los segmentos del donut de desenlaces
siguen sumando `total_runs`, y su segmento `awaiting_approval` sigue contando los
mismos runs aparcados que la tarjeta de aprobaciones, porque esos tres vienen del
mismo lado del interruptor.

La única consulta sin filtro de delegación alguno es el recuento de los runs de
quien llama aparcados en una decisión. Un hijo aparcado es un padre atascado, y esa
tarjeta responde a "por qué no termina mi agent"; hoy no cambia nada, porque una
delegación se escribe en la base de datos ya terminada y por tanto nunca se aparca.

Las dos últimas son `?run=<id>` en la página Activity: un run, las delegaciones bajo
él cada una con la insignia del id de tarea que llevaban sus frames `subagent_*`, y
un enlace hacia arriba al run al que se cargó una delegación. Un panel de delegación
en un chat enlaza ahí con el `run_id` que lleva su frame terminal — razón por la que
el frame lleva uno. Anidar las filas delegadas dentro de la tabla de nivel superior
aquí deliberadamente _no_ se hace; un componente de tabla compartido por
todo el producto está
[propuesto aparte](https://github.com/vstorm-co/agenticos/issues/139), y el anidado
pertenece a ese y no a una tabla de runs hecha a medida.

### Qué muestra la pantalla de coste { #what-the-cost-screen-shows }

`GET /spend` toma su ventana de dos maneras, porque la página pide las dos: `days`
para los preajustes de _últimos N días_, y `from`/`to` para _este mes_, _el mes
pasado_ y un rango de calendario. `from` gana cuando llegan ambos — un rango
explícito es una petición más específica que un valor por defecto que nadie cambió —
y `period_days` vuelve nulo en ese caso en vez de repetir un número que el rango
contradice.

**Cada panel de la pantalla lee la misma ventana.** Las filas por agent, By provider
y By key toman todos el `since`/`until` ya resuelto y no un recuento de días propio,
así que dos cifras contiguas no pueden acabar describiendo runs distintos. Ese es el
mismo defecto que la #198 nombra un panel más arriba.

**El mes en curso ignora la ventana por completo**, y también lo hace cada tope por
agent medido contra él. Un techo mensual comparado con siete días móviles se lee como
un 20% usado el día en que el tope se alcanzó de verdad.

Cada fila por agent lleva **dos cifras de coste bajo dos nombres distintos**, que es
la regla de esta página de principio a fin:

|  |  |
| --- | --- |
| `cost_usd` | Su parte de la ventana, **solo runs de nivel superior**, así que la columna suma el total de encima |
| `month_to_date_usd` | Su **propio** mes natural, filas delegadas **incluidas** — el gasto del que su `monthly_cap_usd` es tope. No suma el mes de la organización y no se dibuja como si lo hiciera |

`partial_run_count` dice cuánto de todo eso es un hecho: cuántos **runs de nivel
superior** de la ventana no se pudieron tarifar del todo, de modo que el coste es un
suelo por exactamente esa cantidad. _"3 de 40 runs no se pudieron tarifar"_ es algo
sobre lo que alguien puede actuar; una cifra con un signo de más no lo es.

Un run cuenta cuando cualquier modelo de su árbol no tenía precio, **los de sus
delegados incluidos** — el árbol comparte un ledger, así que un delegado sin tarifar
vuelve suelo también la fila del padre. Eso permite que una cifra gobierne los tres
desgloses: By provider y By key suman el gasto propio de cada fila, filas delegadas
incluidas, y un suelo en cualquiera de ellos queda marcado por un recuento que nunca
miró la fila causante. **Mide** By agent, que cuenta esas mismas filas superiores, y
solo **marca** los otros dos: cuenta árboles, así que un padre con tres delegados sin
tarifar se lee `1` mientras tres cifras debajo son un suelo.

Un árbol que **cruza el inicio de la ventana** — la fila del delegado dentro, la de
su padre antes — se cuenta por el delegado: la fila del padre, que si no llevaría la
marca, queda fuera de los agregados de la página, mientras el gasto propio del
delegado está dentro de ambos repartos. Aterriza en el agent como el que corrió el
delegado, una vez por árbol que cruce, por muchas delegaciones que crucen el borde, y
solo cuando las peticiones propias del delegado quedaron sin tarifar — una delegación
tarifada bajo un padre sin tarifar fuera de la ventana no levanta salvedad, porque el
dinero de la ventana es exacto
([#620](https://github.com/vstorm-co/agenticos/issues/620)).

Una fila es **una por agent**, con `agent_name` en ella. Antes era una por agent _y
modelo_, llevando solo `model_label` — así que la pestaña listaba nombres de modelo
donde quien lee espera un agent, y partía un agent en dos filas por haber respondido
en dos modelos. La forma por modelo sobrevive donde es la pregunta que se hace: el
correo de uso sigue agrupando así.

**Quién lo gastó es un cuarto desglose**, debajo de By provider, By key y By agent —
el que responde con personas en vez de con proveedores o agents.

Lee las mismas filas de `group_by=user` que la tabla de adopción del dashboard —
solo runs de nivel superior, los más activos primero — así que el coste de un
delegado aterriza una vez, dentro del run que lo inició. Y cubre la ventana que
muestra el resto de la pestaña, en vez de un valor móvil propio.

**Cuándo se gastó** es una barra por día UTC en la misma ventana, desde `by_day` — el
coste propio de cada run, así que los días suman la misma factura que reparten los
desgloses. Un día sin runs se dibuja en cero en vez de omitirse. Cada desglose muestra
la parte de cada fila en su propio total, y un agente con tope mensual muestra el mes
hasta ahora frente a ese tope.

Nombrar a las personas de la organización es la misma decisión que toma la tarjeta
del dashboard, así que toma la misma puerta: `runs:view`, que tienen builder y
operator además de los dos responsables, y lo dice en su propio texto.

Quien no tenga `runs:view` no la ve. La tarjeta está ausente y su pregunta no se hace
nunca, en vez de una petición que vuelve rechazada.

### Acotar la cola de aprobaciones { #narrowing-the-approvals-queue }

`GET /approvals` sirve dos vistas de las mismas filas. Solo las pendientes por
defecto, que es la cola sobre la que alguien actúa; `?status=approved&status=rejected`
es el registro de lo que se decidió, y lleva el nombre y la nota de quien decidió
porque un UUID pelado no es un rastro de responsabilidad. Deliberadamente no hay
controles en una fila ya resuelta.

| Parámetro |  |
| --- | --- |
| `status` | Se repite. Ausente significa pendientes — la cola |
| `triggered_by_user_id` | De quién son los runs que aparcaron la llamada. Se lee de `agent_runs`: una aprobación pertenece a un run y un run pertenece a una persona. Deja fuera los runs en que el publicador sustituyó a una cuenta de chat sin enlazar; esas filas llevan en su lugar `asked_in_chat` (plataforma, nombre y bot), y sus campos `triggered_by_*` quedan vacíos |
| `created_from`, `created_to` | Cuándo se aparcó la llamada, inclusive por ambos extremos |
| `oldest_first` | Por defecto verdadero, y el valor por defecto es determinante — mira arriba: nada hace caducar una llamada, así que el orden inverso enterraría la fila que más necesita verse |

Cada fila nombra tres cosas que viven en otras tablas — el agent, la persona cuyo run
aparcó la llamada, y la persona que decidió. El agent y el run son inner joins porque
ambas claves foráneas hacen cascada, así que una aprobación no puede sobrevivir a
ninguno de los dos; las dos personas son outer joins, porque una decisión tiene que
sobrevivir al borrado de la cuenta de quien decidió y quien visita un widget es
anónimo de entrada.

### Acotar el historial de runs { #narrowing-run-history }

| Parámetro |  |
| --- | --- |
| `status` | Se repite. `?status=failed&status=budget_exceeded` es la consulta de enséñame-los-problemas, y los dos son estados separados precisamente para que pedir uno no sea pedir el otro |
| `surface` | De dónde vino el run |
| `user_id` | Como quién corrió el run, que no siempre es quien preguntó — los runs de un widget llevan la identidad de su propietario, porque la visita es anónima |
| `model_label` | El modelo **tal como lo registró el run**, con coincidencia exacta. No resuelto por el catálogo de modelos: la columna es lo que respondió, y el perfil del que salió puede haberse renombrado o borrado desde entonces. La tarjeta de modelos del dashboard cuenta esas mismas cadenas, así que "los runs detrás de esta barra" son un mismo conjunto en las dos pantallas |
| `started_from`, `started_to` | Inclusive por ambos extremos, porque un selector de rango entrega días enteros |
| `environment_id` | Runs sobre la versión que fija ese entorno. **Nunca un run delegado:** la versión de un delegado viene de un pin, así que la columna deliberadamente no se escribe nunca en uno, y acotar a `production` descarta cada delegación. Una superficie que incluya delegaciones tiene que decirlo |
| `exposure_id` | Runs admitidos por un binding. Nulo para el dashboard y la API |
| `agent_version_id` | Runs que ejecutaron un spec congelado — el "enséñame las filas detrás de este número" de la tira de versiones |
| `rated` | `down` o `up` — runs en los que alguien valoró un mensaje que produjo el run |
| `order_by`, `descending` | `started_at` (el valor por defecto, más recientes primero), `duration`, `cost` o `tokens` |

**Cada filtro acota el recuento además de la página**, así que `total` describe
siempre las filas que tiene debajo. La lista y el recuento son dos consultas, y un
filtro que alcanza solo una de ellas se lee como un fallo de paginación y no como una
cláusula que falta.

`started_from` es además lo que hace ese recuento reconciliable con el dinero que
tiene al lado. Sin ventana se lee _desde siempre_ mientras una cifra de gasto lee un
mes natural, así que una organización de tres años mostraba "8.412 runs" junto a
"$31,20" y la lectura evidente del par se equivocaba en tres años. Una cifra y una
cifra de gasto en una pantalla comparten una ventana, o dicen qué ventana es cada
una.

Un valor fuera de su tipo se rechaza con un 422 en vez de compararse contra nada:
`status` y `surface` son columnas de texto, así que `?status=complete` respondería si
no con una página vacía — y una página vacía se lee como _no ha ido nada mal esta
semana_. `order_by` toma uno de cuatro órdenes y no un nombre de columna, por la
misma razón más otra: un `ORDER BY` montado desde una query string es una superficie
de inyección.

**Todos estos viajan en la URL**, que es lo que permite a una tarjeta del dashboard
ceder el paso a sus propias filas: `/runs?surface=mattermost&period=30d` abre Activity
con la faceta ya puesta y el recuento coincidiendo con la tarjeta que enlazó. Eran
estado local hasta la #768, así que la cifra p95 era el único número del dashboard
que podía alcanzar los runs detrás de él y tres tarjetas no llevaban enlace alguno —
no había nada honesto a lo que apuntarlas.

**Incluida la pestaña que está abierta.** `?tab=approvals` y `?tab=spend` abren la
cola y la pantalla de coste; el historial de runs es el valor por defecto y nunca se
escribe.

Esa es la dirección que necesita un enlace a una _decisión_, y la razón de que el
parámetro exista: el "See all" de la tarjeta de aprobaciones y la alerta que dice que
un run está aparcado tenían ambos que apuntar al historial de runs, donde no se puede
decidir nada (#934).

Una pestaña nombrada por un enlace se **resuelve contra lo que quien lee puede
abrir**. `approvals` está protegida por `approvals:decide`, así que un enlace que la
lleve y alcance a alguien sin el permiso abre el historial de runs en lugar de una
tira cuya pestaña seleccionada no tiene contenido.

Cambiar de pestaña cierra un detalle de run abierto y se lleva `?run=` con él. Un
panel que sobrevive a la pestaña que lo abrió queda al lado de una cola con la que no
tiene nada que ver — y por debajo de `lg` sustituye a la lista, así que la tira seguía
viva mientras el contenido de cada pestaña estaba oculto.

**La duración se calcula en SQL, sobre todo el conjunto acotado.**

Eso es lo que lleva de _"el p95 es 14,8 s"_ en el dashboard a **esos runs**. Ordenar
una página de veinticinco ordena el conjunto equivocado, porque el run más lento de un
mes no está en las filas que devolviera una página de más recientes primero. `cost` y
`tokens` son la misma disposición para el dinero y el peso del contexto.

Un run sin `ended_at` se ordena **el último en ambos sentidos bajo los tres**. No
tiene duración, tampoco es el run más rápido, y sus cifras de coste y tokens se
escriben solo al terminar — ordenado tal como está guardado, un run aún en marcha se
leería como el más barato y ligero de la organización.

Cuánto lleva corriendo un run _todavía en marcha_ es una pregunta distinta, y ninguno
de estos órdenes la responde.

Activity saca esa duración de tres maneras, y las tres llevan a la misma consulta:

- La cabecera de la columna **Took** es un control de orden — como la cabecera Started
  a su lado, y como cada cabecera ordenable del producto — así que un clic reordena el
  historial por `duration` y no por las veinticinco filas en pantalla.
- La cifra **p95 del dashboard enlaza aquí**, ordenada por duración sobre la misma
  ventana: `?sort=duration` con el `started_from` / `started_to` del periodo.

Así el número y los runs que hay detrás están a un clic — la regla que el resto de
estas dos páginas ya sigue, y la única dimensión en la que no lo hacían (#210).

**`rated=down` es la cola de mayor señal de aquí** — las respuestas que personas
reales dijeron que estaban mal, con sus propias palabras. Una valoración cuelga de un
mensaje, así que este join pasa por `messages.run_id`: dos runs en una conversación
conservan sus propias valoraciones, y por eso existe esa columna en vez de una ventana
de tiempo sobre el hilo. Es un `EXISTS`, así que un run que disgustó a tres personas
es una fila y no tres; y un run que gustó a una persona mientras disgustaba a otra
coincide con **ambos**, `up` y `down`, porque los dos son ciertos de él. Reducir eso a
un veredicto por run inventaría un consenso que las filas no registran.

El mismo hecho viaja en la fila sin el filtro. `AgentRunRead.down_rated` es `true`
cuando alguien valoró por debajo de cero una respuesta que produjo el run, calculado
para una página en una consulta, y es sobre lo que el historial de runs dibuja un 👎.
Está acotado a la organización de quien llama como toda lectura de aquí — el run de un
vecino, valorado negativamente, nunca se marca para otro tenant.

El **comentario** que acompañó a ese pulgar se lee en el detalle del run (`?run=<id>`),
no en la fila. Es texto escrito por una persona sobre una conversación, y ponerlo
detrás del detalle es la línea deliberada entre un marcador que ve cualquiera con
`runs:view` y las palabras que lo explican.

Ese es el join para el que se construyó `rated=down`: el dashboard dice que la calidad
cayó cuatro puntos, y aquí es donde se leen las conversaciones que la bajaron.

La tendencia que lee el dashboard es `GET /api/v1/ratings/summary` (un reparto de
titular más una serie por día): `scope=org` bajo `runs:view`, `scope=own` para las
conversaciones propias de un miembro, la misma regla de alcance y el mismo vocabulario
de ventana que `GET /stats/usage` (mira [Permisos](permissions.md)). Solo recuentos —
los comentarios se quedan detrás del detalle del run de arriba.

Las tres cifras de Activity sobre las pestañas siguen siendo las de la organización,
incluido el recuento de runs, aunque la tabla de abajo esté acotada a un agent. Un
recuento por agent junto al mes de la organización serían dos preguntas bajo una
etiqueta — y el recuento por agent es el que incluye delegaciones.

**Una delegación huérfana se informa sin su manejador.** `parent_run_id` es
`ON DELETE SET NULL`, así que borrar al padre deja una fila que correctamente empieza
a contar hacia la factura — pero una clave foránea solo puede anular su propia
columna, y el `subagent_task_id` guardado nombra entonces una transcripción que se fue
con el padre. `AgentRunRead` lo retiene siempre que `parent_run_id` sea nulo, así que
ninguna superficie ofrece un manejador de delegación que no alcanza nada.

### Exportar a CSV { #exporting-to-csv }

Todo lo que muestran las tres pestañas se puede sacar de la pantalla como CSV: las
filas que alguien concilia contra una factura, entrega a un equipo financiero o
adjunta a una auditoría. Una página que puede responder la pregunta en pantalla y no
fuera de ella manda a la gente a la base de datos.

| Pregunta | Respuesta |
| --- | --- |
| `GET /runs/export` | El historial de runs, los mismos filtros que `GET /runs` y el mismo valor por defecto de solo nivel superior. `runs:view` |
| `GET /approvals/export` | El registro de aprobaciones, los mismos filtros que `GET /approvals`. `approvals:decide` |
| `GET /spend/export` | El desglose de gasto por agent, la misma ventana que `GET /spend`. `runs:view` |
| `GET /audit/export` | El rastro de auditoría sobre una ventana, CSV o JSONL (`?fmt=`). `audit:read` |

La exportación de auditoría es la única que ofrece además **JSONL**
(`?fmt=jsonl`), un objeto JSON por línea, porque un rastro de auditoría se
ingiere en un pipeline de logs tan a menudo como se abre en una hoja de cálculo;
ambos describen las mismas entradas, con `details` aplanado a una cadena JSON en
la celda del CSV y conservado como objeto anidado en las líneas.

Lleva exactamente los campos que expone el modelo de lectura de `GET /audit` — la
`ip_address` almacenada no está en esa pestaña, así que tampoco en la exportación
— y, como toda exportación aquí, registra su propia lectura en el rastro
(`audit.export`, nombrando la ventana, el formato y el número de filas).

La exportación de gasto lleva solo las cifras de la ventana — `cost_usd`, `run_count`
y `partial_run_count`. El `month_to_date_usd` y el `monthly_cap_usd` de la pestaña
Spend se quedan fuera: leen el mes natural mientras `cost_usd` lee la ventana de la
exportación, y dos columnas de dólares sobre dos bases de tiempo en un mismo archivo
descargado acaban sumadas por quien no ve la diferencia. Un archivo descargado lleva
una base de tiempo, la ventana que se pidió.

Una exportación es una lectura masiva, no un botón, y responde a seis preguntas que
las rutas de listado no tienen por qué responder:

- **Tenencia.** Cada exportación lleva la puerta de la pestaña de la que viene, y
  toda lectura está acotada a la organización de quien llama — las filas de un vecino
  nunca la alcanzan, incluida una fila que quien llama posee en una organización que
  no es aquella desde la que pregunta. Las dos que van con `runs:view` aplican además
  un **suelo `Scope.OWN` en la consulta**: quien tenga un `runs:view` que alcance
  menos que toda la organización exporta solo sus propias filas, `WHERE user_id =
<ellos>`, y un `user_id` que pase se sobrescribe con el suyo en vez de ensancharlo.
  Ningún rol integrado tiene `runs:view` por debajo de `all` todavía; el suelo está
  puesto para cuando aterrice la decisión de alcance de member/viewer.
- **Tamaño.** Una exportación no tiene techo por naturaleza, así que se le da uno por
  diseño. El **rango de fechas es obligatorio** — una petición sin ambos extremos se
  rechaza — y la coincidencia está **limitada a 10.000 filas**, por encima de las
  cuales la petición se rechaza con un mensaje que nombra el recuento y le dice a
  quien llama que acote el rango. Nunca un truncado silencioso: un CSV recortado es
  peor que uno rechazado, porque una hoja de cálculo suma lo que le llegue. El tope es
  lo que permite construir el cuerpo de una pasada y confirmar la entrada de auditoría
  antes de que salga la respuesta, en vez de enviarla por una conexión abierta.
- **Coste parcial.** `cost_is_partial` es su propia columna en la exportación de runs
  y `partial_run_count` la suya en la de gasto, así que un suelo sobrevive a la suma
  de una hoja de cálculo. Un run cuyo único modelo no estaba tarifado exporta su
  `cost_usd` real de `0` junto a `cost_is_partial=true` — nunca un `0` pelado que
  quien lee toma por gratis.
- **Runs delegados.** La exportación de runs toma por defecto solo filas de nivel
  superior, exactamente como la lista, así que sumar `cost_usd` da la factura y no el
  doble. La postura está en el archivo, no solo aquí: cada fila lleva una columna
  `parent_run_id`, vacía para un run que alguien inició y puesta para una delegación,
  de modo que quien opte por `include_delegations` vea qué filas contarían doble si se
  sumaran enteras.
- **Datos personales.** Cada exportación entrega exactamente la identidad que ya
  muestra su pestaña. La tabla de runs muestra un `user_id` y ningún nombre, así que
  la exportación de runs entrega solo el id — un CSV de quién-ejecutó-qué con los
  nombres resueltos es la tabla por persona que rechazó la decisión 3 del diseño de
  actividad, llegando como descarga. La cola de aprobaciones ya resuelve en pantalla
  los correos de quien disparó y quien decidió, así que la exportación de aprobaciones
  los conserva. También lleva el nombre visible o el handle de un desconocido del chat, tal como lo muestra la cola, en tres columnas: `asked_in_chat_platform`, `asked_in_chat_account` y `asked_in_chat_bot`. En esa fila las dos columnas de correo quedan vacías, porque el publicador no preguntó.
- **Auditoría.** Cada exportación escribe una entrada de `audit_log` — una lectura
  masiva privilegiada, barata de registrar ahora e imposible de reconstruir después.
  Nombra la ventana, los filtros aplicados y el número de filas, nunca el cuerpo de la
  petición.

### Un delegado fijado no se mueve solo { #a-pinned-delegate-does-not-move-on-its-own }

Un delegado está fijado a una versión, así que que su autor publique un arreglo no
cambia nada para quienes lo llaman hasta que alguien republique al padre contra el
nuevo pin. Esa es la misma garantía que da publicar en todo lo demás de aquí, y corta
por los dos lados: un fallo arreglado en un delegado es un fallo todavía vivo en cada
padre que no se haya movido.

El Builder es donde eso se saca a la luz — compara cada pin con lo que el delegado
publica ahora y ofrece moverlo — porque una obsolescencia que nada muestra es un fallo
congelado en su sitio. Un pin cuya versión ya no existe **hace fallar el run** y
nombra al delegado; nunca una vuelta silenciosa a la versión actual.

**Archivar un delegado deja de responder, incluso como delegado de alguien.** Un pin a
un agent ya archivado se rechaza al publicar, y un agent archivado después de haber
sido fijado hace fallar por su nombre el run de quien lo llama — si no, retirar un
agent del servicio lo dejaría corriendo indefinidamente en el único sitio donde nadie
mira, y quien lo jubiló no se enteraría nunca.

### Límites de pasos { #step-limits }

La otra clase de desbocamiento es un bucle de herramientas: barato por llamada, y no
termina nunca. Un budget solo factura eso. `max_steps` limita cuántas peticiones al
modelo puede hacer un run y es lo que de verdad lo detiene.

El guardián de bucles detiene la mayoría antes, y dice por qué. Lo tiene cada
agente, activado por defecto. Cuando un run llama a una herramienta con los
mismos argumentos cuatro veces seguidas, o recorre tres veces un ciclo de dos o
tres llamadas, se le dice al modelo que está en un bucle y que cambie de enfoque.
Si las llamadas tras ese aviso vuelven a entrar en bucle, el run termina como
`loop_stopped`, con un motivo que nombra las herramientas - nunca sus
argumentos.

Llamadas con argumentos distintos nunca son un bucle, y solo cuentan
las llamadas del propio run. El Builder fija ambos umbrales, o apaga el
guardián, junto a `max_steps` (`loop_guard` en el spec).

### Revisores del run { #run-reviewers }

Un run largo deriva: las instrucciones se desvanecen, se acumulan afirmaciones sin
fuente, el agente se aleja de la petición. Un agente puede llevar hasta cinco
revisores (`reviewers` en el spec, _Revisores del run_ en el Builder junto a los
límites), cada uno un segundo modelo, normalmente más barato, con un solo
cometido. Cada `every` peticiones al modelo un revisor lee los últimos `window`
tokens del run y lo deja seguir o envía una corrección breve, que el modelo del
run recibe en su siguiente petición con el nombre del revisor. La vista del run
lista cada corrección y quién la envió.

Las llamadas de un revisor se comprueban contra el budget del agente y de la
organización antes de hacerse y se anotan en el run, al precio del modelo del
revisor. Una rechazada por el budget se omite, y un revisor que falla queda
registrado; ninguna de las dos hace fallar el run. Un agente sin revisor no hace
llamadas de modelo adicionales.

### Modo código { #code-mode }

Un agente que recorre registros - actualiza diez filas y luego las resume - paga
un turno de modelo por cada llamada dependiente, y cada turno reenvía todo el
contexto. Con el _Modo código_ activado (`code_mode` en el spec, junto a los
límites en el Builder) las herramientas del agente se pliegan en una sola
herramienta `run_code`: el modelo escribe un script corto de Python que corre en
el sandbox Monty y llama a las herramientas en bucles y en paralelo, hasta
`max_tool_calls` por script.

Como mucho cuatro llamadas de un script corren a la vez y el resto espera, así que
un script que reparte cientos de llamadas en el sandbox no puede abrirlas todas
juntas.

Cada llamada desde un script pasa por los mismos guardrails, el mismo budget y
las mismas comprobaciones de permisos que una llamada directa. Una herramienta
que necesita aprobación nunca se pliega: sigue siendo una llamada normal, y el run
se detiene para la persona como siempre. Lo mismo vale para preguntas,
delegaciones y herramientas cuya llamada es lo que ve la persona (borradores,
gráficos, dibujos). La vista del run lista cada script y
cada llamada que hizo.

### Informes { #reporting }

Un run que no se pudo tarifar — un modelo que `genai-prices` no conoce — se registra
a cero con un aviso y el total se marca como un **suelo** en vez de adivinarse. La UI
lo muestra como un `+` junto a la cifra.

**Las lecturas y escrituras de caché se llevan aparte.** Los tokens de entrada de
un run incluyen los que el proveedor sirvió desde su caché de prompts y los que
escribió en ella, y los tres se tarifan aparte - en Claude Sonnet 5.5 una lectura
de caché cuesta el 5 % de la tarifa de entrada y una escritura el 125 %. Cada run
y cada respuesta guardan `cache_read_tokens` y `cache_write_tokens` junto al
total, el coste se calcula a partir de ese reparto, y la vista del run y el coste
bajo cada respuesta lo muestran - que es lo que concilia un run con la consola del
proveedor.

**El coste de un run incluye sus delegaciones.** El run que alguien inició lleva
todo el ledger, delegados incluidos; la fila propia de una delegación reparte el
mismo dinero por agente, y la vista del run dice que sus delegaciones ya están
dentro de la cifra de arriba. Sumar ambas cuenta las delegaciones dos veces.

**Una caché de prompts que deja de acertar se señala.** La caché solo compensa
mientras el principio de cada petición sigue idéntico; una lista de herramientas
reordenada o una marca de tiempo en las instrucciones hacen que el proveedor
cobre a precio completo tokens que tenía en caché. Cada agente lee las cifras de
caché del proveedor en cada respuesta, y una petición cuyo prefijo en caché se
hundió se marca en la cascada de peticiones del run - _caché rota_ cuando el
proveedor aún debía conservarla, _caché caducada_ cuando pasó su retención - con
cuántos tokens en caché no leyó, y se registra con el agente y el modelo.
La primera petición de un turno se compara con lo que guardó el turno anterior,
cuando el mismo worker de API atiende ambos.

Los recuentos se guardan en cada run, así que **Runs → Spend** suma dónde no
aguantó la caché: una tarjeta _Prompt cache_, una fila por agent y modelo, con
sus busts, expiraciones y los tokens en caché que no se leyeron de vuelta en la
ventana - una medida del fallo, no de lo que se volvió a cobrar, ya que una
compactación también puede acortar el prompt que la caché guardaba.
La tarjeta no aparece mientras todas las cachés aguantaron.

## Aprobaciones { #approvals }

Una herramienta que el autor del agent puso tras una puerta aparca el run y espera
a una persona. Nada queda tras una puerta hasta que alguien lo pide: una puerta que
nadie eligió aparca runs en canales y rutinas donde no hay nadie para responderla.

La resolución va de lo más específico a lo menos:

1. la anulación de la propia herramienta, si la tiene
2. el modo `approval` de la capability (`required` | `never` | `default`)
3. ninguna aprobación, cuando ambos dicen `default`

`side_effecting` ya no decide nada aquí. El Builder lo muestra junto a la elección,
así que las herramientas que actúan sobre el mundo son las que un autor ve primero.

El Builder enuncia el resultado con palabras en vez de describir la regla, porque una
regla que quien lee tiene que ejecutar mentalmente es un ajuste que nadie se atreve a
tocar.

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

Cinco propiedades que conviene conocer:

- **Un run aparcado es reanudable.** Su historial de mensajes está guardado, así que
  la decisión se aplica a la conversación a la que pertenece en lugar de empezar de
  nuevo.
- **Un run aparcado sobrevive a una recarga y lo dice.** La transcripción guarda la
  llamada en la que el run se detuvo como `awaiting_approval` y no como `running`, así
  que reabrir la conversación sigue mostrando el paso a la espera — y
  `GET /runs/{id}/parked` responde con las llamadas pendientes (la aprobación a
  decidir, la herramienta, sus argumentos), que es como el chat reconstruye el panel
  de aprobación que el frame en vivo `tool_approval_required` dio a quien estuviera
  mirando. Responde vacío para un run que no está aparcado, y está protegido por
  `approvals:decide` como la cola, porque sus filas se ofrecen para ser decididas
  ([#601](https://github.com/vstorm-co/agenticos/issues/601)). El paso no se lee como
  a la espera para siempre: una reanudación lo resuelve con lo que devolvió la
  llamada, y una caducidad lo resuelve con el aviso de tiempo agotado.
- **Una continuación dice lo que hizo.** `POST /runs/{id}/resume` responde con las
  llamadas a herramientas que hizo la continuación, en orden, cada una con lo que
  devolvió — y la transcripción las registra llegara o no a una respuesta. Antes
  faltaban ambas mitades, y una aprobación podía ocultar una cantidad ilimitada de
  trabajo: el agent corría dentro de la petición de reanudación y no sobre el socket
  por el que se transmite la conversación, así que nada anunciaba sus llamadas, y la
  escritura de la transcripción se omitía para un segmento sin respuesta. Un run que
  leyó un archivo y luego pidió ejecutar un segundo comando no mostraba nada entre las
  dos aprobaciones ni registraba nada.
- **Y lo que devolvió la llamada aprobada aterriza en el paso que se aprobó.** Llega
  aparte de las llamadas propias de la continuación (`settled`, no `steps`), porque la
  hizo la ejecución que se aparcó: la reanudación produce su retorno sin la llamada a
  la que pertenece, así que cierra una fila ya escrita en vez de abrir una nueva.
  Registrarlo como un paso pondría el mismo comando dos veces en el turno; no
  registrarlo en absoluto — que es lo que pasaba hasta que se hizo — convertía la
  única llamada que alguien revisó deliberadamente en la única llamada sin salida en
  ninguna parte.
- **Sigue siendo reanudable si continuarlo falla.** Un run se continúa sobre la
  versión en la que se aparcó, y el spec de esa versión puede haber dejado de
  construirse desde entonces — un secreto que nombra un binding borrado, un perfil de
  modelo eliminado, una capability caída en un despliegue, una conexión MCP dejada de
  compartir. El spec se ensambla antes de que el run salga de la cola de aprobaciones,
  así que un rechazo ahí rechaza el _intento_: la decisión se mantiene y reanudar
  vuelve a funcionar en cuanto el spec funcione. Un run planteado en un chat que nadie
  continuó lo termina la red de seguridad aproximadamente una hora después de su
  última decisión.
- **Quién continúa un run planteado en un chat.** La decisión de una persona en la
  consola, en el chat web o por la API continúa el run en su chat, como ella, y la
  respuesta se publica allí; la decisión de una clave se registra y no continúa
  nada. Una continuación puede perderse o ser rechazada, y entonces el run espera en
  `awaiting_approval` sin nada pendiente. La red de seguridad horaria termina un run
  de canal así `STRANDED_AFTER` (una hora) después de su última decisión y se lo dice
  al chat una vez - no cuando la decisión tiene más de 7 días, ni cuando el chat ya
  lo supo. Solo mira runs de canal. Véase
  [Decidido fuera del chat](channels.md#decided-outside-the-chat).
- **Una aprobación resuelta no se puede decidir dos veces.** La segunda decisión se
  rechaza — incluida una decisión que llegue un segundo después de que el barrido de
  caducidad se llevara la llamada.
- **Una llamada aparcada se deniega por tiempo en cuanto pasa de
  `APPROVAL_EXPIRY_HOURS`**, y el run que hay detrás se resuelve en vez de quedar
  aparcado para siempre. El estado es `expired` con un `decided_by_user_id` nulo, que
  es lo que distingue una caducidad de un rechazo en el rastro de responsabilidad. Un run planteado en un chat también le
  dice una vez al chat que la petición caducó. La
  página Activity sigue mostrando la **edad** de la espera más antigua, porque una cola
  dentro de su ventana de caducidad es aquella sobre la que alguien todavía puede
  actuar.
- **`required` funciona en cualquier capability**, no solo en las que tienen efectos
  externos. "Esto solo lee, pero en mi organización alguien lo aprueba igualmente" es
  una decisión real y expresable.
- **Salvo en una herramienta que ejecuta el provider del modelo, donde se rechaza al
  publicar.** La puerta envuelve la _ejecución de la herramienta_, que es el único
  sitio donde se puede retener una llamada, así que una búsqueda o una descarga
  nativas — ejecutadas del lado del provider — nunca la alcanzan, y controlar una
  dejaría la cola vacía mientras el agent actuaba sin aprobación. Qué configuraciones
  entregan qué herramientas lo declara la propia capability (`provider_executed` en su
  `register(...)`), así que el rechazo cubre cada capability a la que le crezca un
  método ejecutado por el provider y no las que un validador conociera
  ([#857](https://github.com/vstorm-co/agenticos/issues/857)). Elige un método que
  ejecute este deployment, o quita el requisito de aprobación; ambos son agents
  legítimos, y cuál se quiere no es una decisión que tomar en nombre de su autor.
- **Y una versión publicada antes de que existiera ese rechazo no se ejecuta.** Nada
  revalida una versión congelada — un run carga su spec guardado y lo ensambla — así
  que la misma comprobación vuelve a correr cuando se construye el agent, y rechaza en
  vez de cambiar en silencio el método para que la puerta funcione. El coste es real y
  deliberado: un agent que llevaba tiempo funcionando así se detiene, con un mensaje
  que dice qué cambiar. Lo que se detiene es un agent cuyo operador pidió una
  aprobación que nunca se le estaba pidiendo a nadie.
- **Una llamada aparcada puede nombrar lo que representan sus ids.** Una
  herramienta puede describir su propia llamada: las escrituras en tablas dan el
  nombre de la tabla. Nunca
  se nombra un registro, porque la etiqueta de un registro es el texto de una de
  sus celdas, y la aprobación la conservaría. Los nombres se resuelven como la
  persona del run cuando la llamada se aparca, así que no se nombra nada que esa
  persona no pueda ver, y se guardan en la aprobación como `context`. La tarjeta
  del chat y la página Approvals empiezan con una frase construida a partir
  de ellos, como "Share the view Closed tickets of Support with everyone who may
  read the table", encima de los argumentos, que siguen siendo lo que se ejecuta.
  La frase se encuentra por el id estable de la herramienta, así que una
  herramienta que un binding renombró la conserva. Una descripción que falla o
  tarda aparca la llamada sin ella, y una llamada que un run reanudado ya decidió
  no se vuelve a describir.
- **Un solo paso de modelo puede aparcar varias llamadas.** Un modelo que responde con
  dos llamadas con efectos externos a la vez — "escribe al cliente y al gestor de
  cuenta" — aparca ambas, cada una con su propia fila de aprobación decidida por
  separado. Las filas se escriben cuando el run se aparca y no según se controla cada
  llamada, porque las llamadas corren concurrentemente y la sesión de base de datos del
  run no es segura para la concurrencia
  ([#169](https://github.com/vstorm-co/agenticos/issues/169)).

### Cuánto quiere una conversación que se le pregunte { #how-much-one-conversation-wants-to-be-asked }

La tarjeta de aprobación también ofrece permitir esta herramienta en este chat o permitirla siempre para este agente. La elección solo se aplica a la herramienta y cuenta indicadas, a la persona que consiente y a la versión publicada del agente. Requiere el mismo permiso y ajuste de organización que aprobar todas las acciones. No autoriza delegados ni ejecuciones desatendidas. Una nueva versión del agente requiere una nueva elección.

Las elecciones se guardan en el servidor. **Chat controls → Preferences → Remembered tool approvals → Ask again** revoca una elección sin borrar la aprobación original. Antes de cada aprobación automática se vuelven a comprobar la pertenencia, el permiso de aprobación y la política de la organización. El modo de preguntar por todo tiene prioridad sobre las elecciones guardadas. Cada aprobación automática sigue registrando quién consintió y los argumentos reales de la llamada.

La regla de arriba es la del agent, decidida al publicar y por herramienta, y ese es
el sitio correcto para ella: es una afirmación sobre lo que el agent _es_. Lo que no
puede expresar es el ánimo de una sesión — alguien que trabaja veinte turnos con un
agent que controla tres herramientas responde las mismas tres preguntas cada turno, y
su única salida era republicar el agent y cambiarlo para todo el mundo, de forma
permanente, para arreglar una tarde
([#925](https://github.com/vstorm-co/agenticos/issues/925)).

Así que una sesión de chat lleva un **modo de aprobación**, en el frame de envío junto
a la anulación de modelo y leído dentro del run:

| Modo | Qué hace |
| --- | --- |
| **Follow the agent** (por defecto) | Decide el spec. Exactamente el comportamiento que existía antes del control, y lo que obtiene un cliente que no envía nada |
| **Approve everything** | Consentimiento permanente para esta conversación: cada llamada controlada se concede sin aparcarse — y cada una escribe igualmente su fila |
| **Ask about everything** | Controla cada herramienta que el agent pueda alcanzar, incluidas las que el spec dejó sin controlar y las que no pertenecen a ninguna capability |

Cuatro cosas hacen de esto un ajuste de sesión y no un agujero en el modelo:

- **Se rechaza, nunca se degrada.** A quien no pueda renunciar se le dice; el turno no
  corre en silencio siguiendo el spec, porque alguien que cree haber apagado las
  preguntas y luego se encuentra un run aparcado ha recibido lo contrario de lo que
  ocurrió. La comprobación vive en `AgentRunnerService.prepare`, el único embudo que
  comparten un run nuevo y uno reanudado, y no en un socket que quien llama podría
  olvidar.
- **Renunciar requiere `approvals:decide`, y el permiso de la organización.** Un
  consentimiento permanente _es_ la decisión que la cola de aprobaciones existe para
  registrar, y `member` y `builder` ejecutan agents sin tenerlo — así que sin la
  comprobación de permiso quien usa el chat a diario se concede a sí mismo, en un
  clic, la autoridad que la API le niega un endpoint más allá. El interruptor propio de
  la organización (`chat_may_waive_approvals`, activado por defecto para las organizaciones nuevas, cambiado por
  alguien con `approvals:decide`) es el techo: sin él, la puerta deliberada de un
  Builder sobre `send_email` está a un clic de nada en cada conversación y el modelo
  por herramienta es orientativo.
- **Ningún canal sigue significando no.** Solo una sesión de chat web puede renunciar.
  Un horario, un webhook, un embed y un canal se rechazan todos, porque `ApprovalGate`
  ya rechaza un run sin nadie a quien preguntar y el consentimiento permanente no debe
  convertirse en la vuelta a eso.
- **Cada llamada con renuncia se registra igualmente.** La fila se escribe como
  `approved`, nombrando la cuenta que consintió, con `decided_via = "standing"` — y el
  registro de aprobaciones lo dice con palabras junto al nombre. Saltarse la fila haría
  indistinguible un run con renuncia de un agent que nunca estuvo controlado, que es
  este rastro entero dejando calladamente de serlo. Nadie leyó esos argumentos antes de
  que se ejecutaran; la fila es donde alguien los lee después.

**Ask about everything hace más estricta la política de la sesión.** Seleccionar
este modo más estricto no requiere permisos especiales. Incluye las llamadas MCP que ejecuta el
runner, aunque sus herramientas no estén declaradas por capabilities. Las listas
de herramientas permitidas de la conexión y del vínculo con el agente controlan
qué herramientas MCP están disponibles; lo que las retiene para una persona es la
política `approval` del propio vínculo - `writes` o `all` -, que el modo
predeterminado **Follow the agent** aplica como la de una capability (ver
[qué llamadas esperan a una persona](mcp.md#which-calls-wait-for-a-person)).

### Una aprobación dentro de una llamada de voz { #an-approval-inside-a-voice-call }

Una [llamada de voz](voice.md#approvals-in-a-call) no puede pausarse y esperar a
la cola, así que sus aprobaciones se deciden en la llamada, por la persona que
está en ella:

- La solicitud se escribe en la cola al instante, y la llamada muestra la
  herramienta, los argumentos exactos con los que se ejecutará y lo que se oyó
  decir a quien llama. Aprobar ejecuta esos argumentos, una vez.
- Se decide **solo a través de la llamada**, y solo por quien llama si tiene
  `approvals:decide`. `POST /approvals/{id}` rechaza una solicitud que pertenece
  a una llamada en curso, y el barrido de aprobaciones caducadas de más abajo se
  salta los runs de voz.
- **60 segundos** sin decisión la caducan, sin nadie que decida: el mismo null
  que escribe el barrido. Una llamada que termina por cualquier motivo caduca lo
  que todavía estuviera esperando.
- A un miembro sin `approvals:decide` se le dice claramente, y la herramienta se
  rechaza. Un visitante en un widget o en una página alojada nunca ve una
  herramienta que necesite aprobación: se deja fuera de su llamada.

`approval_mode` sigue el spec del agent, como en el chat de texto, y cada
decisión llega al registro de auditoría como cualquier otra.

### Una decisión que nadie toma { #a-decision-nobody-makes }

Una aprobación espera a una persona, y algunas esperan para siempre: quien revisaba se
fue, la herramienta se pidió un viernes, nadie sabía que le tocaba decidir. Nada en un
camino de petición puede terminar una — la premisa entera es que no viene ninguna
petición — así que un barrido cada hora deniega por tiempo todo lo que siga pendiente
pasadas `APPROVAL_EXPIRY_HOURS` (tres días por defecto, que cubren un fin de semana).

!!! warning "Lo que importa es el run, no la fila"

    Una aprobación pendiente mantiene su run en `awaiting_approval` indefinidamente:
    trabajo que ni está terminado ni va a estarlo.

Un run así se queda en el historial y en la edad de la espera más antigua del
dashboard, así que el barrido sigue cada llamada caducada hasta el run que hay detrás
y lo termina, `cancelled` — nadie volvió, y lo que gastó antes de aparcarse se
mantiene.

Tres cosas que deliberadamente no hace:

- **No continúa el run.** Una llamada _rechazada_ se resuelve reanudando: la
  denegación se reproduce y el agent sigue hasta una respuesta. Eso es una petición al
  modelo contra las claves propias de la organización, y hacer una según un horario,
  para un run que nadie espera, no es un coste en el que incurrir sin que nadie lo
  pida.
- **No termina un run con una llamada todavía dentro de su ventana.** Un run se aparca
  sobre todas sus llamadas pendientes a la vez, así que se termina solo cuando ninguna
  sigue pendiente.
- **No nombra a quien decide.** `decided_by_user_id` se queda nulo y también el actor
  de la entrada de auditoría, porque ese es el hecho que se está registrando. Un nulo
  ahí significa la plataforma según un horario, y nada más puede producir uno.

Esta es la única lectura del código que cruza todas las organizaciones, por la razón
de que un horario no tiene tenant al que acotarse. Cada escritura que hace sigue
estando en la organización de la propia fila.

### Un run cuyo proceso murió { #a-run-whose-process-died }

El otro estado que nada dentro del proceso va a resolver.

La fila de un run se confirma como `running` antes de llamar a su modelo
([#12][12-issue]), así que un worker muerto a media ejecución — un OOM, un despliegue
que no drena — deja una fila duradera sin nada que la termine: en Activity para
siempre, y bloqueando cualquier horario del que fuera el run enlazado de su trigger.

Mientras su modelo trabaja, el proceso que ejecuta un run lo marca como vivo cada
`RUN_HEARTBEAT_SECONDS`. Un barrido cada minuto termina como **`failed`** todo lo
que siga en `running` sin señal de vida durante `STALE_RUN_REAPED_AFTER_SECONDS` —
dos minutos por defecto, cero lo apaga — y avisa a las consolas abiertas, así que
un chat deja de mostrar "Working" sobre un turno que nadie responde y ofrece
preguntar de nuevo. El run de una llamada de voz queda para el reaper de llamadas,
que la cuelga. Nadie detuvo
este run, lo hizo la infraestructura, y quien filtra el historial de runs buscando
problemas es exactamente quien debería verlo.

El error de la fila es la frase del propio barrido. El proceso que sabía más murió.

La edad de un run aquí es su **última transición**, no su primer inicio. Una
reanudación conserva el `started_at` original — el run abarca ambos segmentos — así
que un run aprobado días después de aparcarse envejece desde el momento en que empezó
su reproducción, y no desde un inicio que lo haría segarse a media reproducción.

El techo tampoco tiene que ser exacto en ningún sentido, porque un run vivo que el
barrido voltee igualmente se vuelve a voltear solo: su propia escritura terminal
aterriza más tarde y gana.

Lo que un run segado no puede recuperar es su **gasto**. El ledger murió con el
proceso, así que la fila se queda con los ceros con los que se abrió en vez de recibir
un número que alguien conciliaría contra una factura.

Y no se le escribe a nadie. La notificación de fallo viaja en `finish`, que tiene el
agent y su spec a mano; un barrido no tiene ninguno de los dos.

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

### Una aprobación dentro de una delegación { #an-approval-inside-a-delegation }

Las herramientas de un delegado las controla el spec del propio delegado, y llega a la
misma cola en la que ya espera quien llamó al padre — un especialista que necesita a
una persona necesita a la persona que está ahí.

La entrada nombra la herramienta **del delegado** y los argumentos que propuso, porque
lo escribió la puerta del propio delegado. Y nombra **qué delegado la está llamando**.

Sin esa última parte la cola dice `send_email` sin decir si lo envía el agent con el
que alguien está hablando o un especialista llamado `researcher`. Esa es una cola que
la gente aprueba a ciegas — y en una delegación lo que se aprueba es a menudo más
trascendente que el agent con el que quien revisa cree estar tratando.

Borrar ese delegado no borra el registro de lo que estaba autorizado a hacer: la fila
conserva el nombre del delegado y pierde solo el enlace a su agent ya desaparecido.
Esto se cumple incluso cuando el borrado llega mientras el run sigue aparcado, antes
de que se haya escrito la fila de aprobación — la escritura diferida
([#169](https://github.com/vstorm-co/agenticos/issues/169)) resuelve los delegados aún
presentes y escribe un id nulo para el que desapareció, exactamente lo que habría
hecho borrarlo después de existir la fila.

Lo que hace el run del padre es aparcarse, en vez de recibir algo que parece una
delegación terminada. Vale la pena decirlo porque antes era de otro modo: cada agent
construido aquí declara un tipo de salida que permite a un run terminar con sus
llamadas aparcadas como _salida_ en lugar de lanzar, y la librería de delegación
serializaba ese objeto y le entregaba al modelo del padre
`{"calls": [], "approvals": [...]}` como el informe del especialista, con la tarea
marcada como completada. Era el camino por defecto y no un caso límite, y está
arreglado en la versión fijada.

Aprobarla **continúa el delegado**, en vez de delegar otra vez. El estado aparcado es
un árbol — un nivel por agent, cada uno con su propia conversación y sus propias
llamadas aparcadas — así que conceder la aprobación reanuda el delegado suspendido
donde se quedó, con el veredicto pegado a la llamada que quien revisa vio de verdad.
La llamada `task` del padre se reproduce, y la delegación encuentra el sitio que
dejó. Un especialista dentro de un delegado se comporta igual, un nivel más abajo.

Eso importa porque la alternativa no es una reanudación más lenta, es una respuesta
distinta. Volver a ejecutar la delegación empezaría la conversación del delegado desde
cero y dejaría a su modelo llamar a otra herramienta la segunda vez, así que lo que
alguien aprobó no sería lo que se ejecutó.

Lo que el delegado ya había gastado viaja con su sitio, así que la fila escrita cuando
la delegación por fin acaba lo cubre todo — mira
[cómo se registra un run delegado](#what-a-delegated-run-is-recorded-as). Ambas
mitades del árbol se guardan por delegación y no por run, que es lo que permite a un
especialista tres niveles más abajo aparcarse y aun así contabilizarse a su propio
agent. El gasto se guarda incluso cuando el _sitio_ del delegado no se pudo guardar —
el historial de mensajes de la librería es telemetría de mejor esfuerzo, y una
delegación reejecutada desde el principio ha gastado igualmente lo que gastó.

!!! warning "La aprobación de MCP se configura en el vínculo"

    El ajuste de aprobación de una capability no cubre herramientas MCP; la
    política `approval` del vínculo sí, en cada superficie en la que corre el
    agente. `writes` retiene las herramientas que el servidor no marca como de
    solo lectura - es la palabra del propio servidor, así que elige `all` para uno
    que no controlas. En el chat web, **Ask about everything** (`ask_all`) también
    controla el resto; no supongas que el ajuste más estricto de una sesión de chat
    se aplica a una programación u otro punto de entrada. Las herramientas
    ejecutadas dentro del proveedor del modelo no pasan por la puerta de
    aprobación local.

### Un paso de workflow que pregunta a una persona { #workflow-step-approvals }

Un workflow pregunta con un paso [`human.approval`](reference/workflow-nodes.md#human-approval)
en lugar de una herramienta protegida: el paso escribe su propia solicitud - un
título, los detalles que recibió, quién puede decidir y hasta cuándo - y su run la
espera. La pestaña **Approvals** las lista encima de las llamadas a herramientas, y
decidir una despierta el run, que sigue según la respuesta.

Una decisión es
definitiva y queda en el registro de auditoría junto con lo que se preguntó, y sigue listada en **Decided** junto al registro de
las llamadas a herramientas. Un
paso que nombra a quienes aprueban solo puede decidirlo esa gente. Una solicitud
que nadie decide a tiempo caduca, y un run cancelado cancela sus solicitudes.

Un paso `editable` también muestra a quien aprueba una propuesta, como una
respuesta redactada, que puede cambiar antes de aprobarla. La entrada de auditoría
registra si el paso era editable, si la persona lo editó (`edited`) y la longitud
de la propuesta y del texto aprobado (`proposal_length`, `final_length`), nunca el
texto en sí. El texto se guarda en texto plano en la solicitud hasta que se borra
el historial del run.

Las solicitudes también se cuentan. La tarjeta Stats de un workflow y su API de
estadísticas informan, por paso y por día, cuántas se aprobaron como se propuso, se
editaron, se rechazaron o caducaron, cuánto tardaron las personas en decidir y cuánto
cambiaron sus ediciones el texto, cada una como una razón de `0` a `1` (`edit_ratio`,
también en la entrada de auditoría); véase
[Running a workflow](workflows.md#running-a-workflow). El resumen de toda la
organización, `GET /api/v1/workflow-approvals/stats`, requiere `workflows:view` y solo
abarca los workflows que el miembro puede ver. No contiene ni la propuesta ni el texto
aprobado ni cifra alguna de quien aprueba: cuenta solicitudes, no personas.

### Entradas de artefactos { #artifact-entries }

Una página que lee datos de tabla se comparte con el consentimiento de su propietario, y
cada paso de eso deja una entrada con la persona que lo dio. Ninguna lleva un registro, un
valor de filtro ni una fila: contienen versiones, huellas, ids de solicitud y las palabras
que una persona confirmó.

| Acción | Se registra cuando |
| --- | --- |
| `artifact.published` | Se publicó una versión, con su número, la huella de sus page queries y `actions`, los ids de los workflows que declara |
| `artifact.data_consented` | El propietario confirmó los datos de una versión, o se consintió automáticamente (`automatic`). Nombra la versión y la huella, `for_owner` cuando un Owner o Admin confirmó por él, y `via_grant` cuando lo dio una concesión `edit_data` |
| `artifact.data_withdrawn` | Un editor dejó de compartir los datos de la página; nombra la versión que se servía a la audiencia, si el consentimiento automático estaba activo y cuántas concesiones `edit_data` bajó a `edit` |
| `artifact.auto_consent_changed` | El propietario activó o desactivó «Share new versions automatically», con el texto que confirmó |
| `artifact.edit_data_granted` | El propietario, o un Owner o Admin, dio a una persona o a un grupo «Can edit, including data», con el texto que confirmó |
| `artifact.owner_changed` | Una página cambió de manos: el propietario anterior y el nuevo, quién lo hizo y las concesiones `edit_data` que se descartaron |
| `artifact.share_requested` | Se presentó una solicitud — de tipo `audience`, `access` o `version` — por una persona o por el run de su agent |
| `artifact.share_request_approved` y `artifact.share_request_declined` | Se decidió una solicitud, y quién |
| `organization.public_live_data_changed` | Un Owner o Admin activó o desactivó **Public pages with live data** para la organización, con el valor anterior y el nuevo |
| `artifact.public_data_consented` | El propietario, o un Owner o Admin en su nombre, confirmó que cualquiera con el enlace público de la página, y los sitios web que la incrustan, puede leer sus datos. Nombra la versión, la huella, las tablas y columnas, los sitios web que incrustan y si hay una contraseña, y `for_owner` cuando alguien lo confirmó en su nombre |
| `artifact.public_data_withdrawn` | Un editor retiró ese consentimiento, con la sesión iniciada como uno mismo (sin suplantar), con la huella que tenía |
| `artifact.public_data_voided` | El consentimiento se borró porque cambió algo de lo que dependía, con un `reason`: `link_off`, `password_removed`, `origin_added`, `owner_changed`, `consent_withdrawn` o `archived` |
| `artifact.action_started` | Un miembro confirmó e inició un workflow desde la acción de una página. Nombra la página, la versión, el workflow y el run, nunca los valores de entrada, que están en el run |

**Public pages with live data** es un ajuste de la organización, desactivado por defecto, en los
ajustes de la organización junto a la exención de aprobación. Decide si los datos de una página
pueden llegar a personas sin cuenta mediante un enlace público o un embed. A diferencia de la
exención, requiere `org:settings` - Owner o Admin - y debe cambiarse con la sesión iniciada como
uno mismo: una sesión que suplanta se rechaza. La página sigue necesitando el
[consentimiento de datos públicos](artifacts.md#live-data-on-a-public-link) de su propietario.
Desactivar el interruptor es un interruptor de emergencia: las páginas públicas pierden sus datos en
la siguiente carga y conservan su consentimiento, que vuelve a valer cuando se activa de nuevo.

Ampliar la visibilidad o las concesiones de una página se registra como entradas
`resource.share`, `resource.unshare` y `resource.visibility_changed`, como en cualquier otro
recurso. La herramienta `share_view` ya no existe, y con ella la aprobación que la protegía.

## Alertas { #alerts }

Cada alerta de aquí trata de un run que nadie está mirando. Un run de chat que se
detiene por su budget lo dice en pantalla; el mismo run iniciado por una mención de
Slack, por un horario o por una llamada a la API se detiene en silencio, y lo primero
que alguien sabe de ello es cuando pregunta por qué el agent se ha callado.

### En la aplicación, junto con el correo { #in-app-alongside-email }

Cada alerta de esta sección escribe dos cosas: el correo que se describe más
abajo, y una fila en **la campana** de [la consola](console.md#the-bell) — el
propio buzón de la consola, no una copia del correo. La fila _es_ la entrega
en la aplicación; no hace falta que nada más tenga éxito para que aparezca. El
correo es un segundo canal, reintentado de forma independiente a partir de la
misma escritura, que es por lo que uno puede fallar — una dirección rebotada,
un relay SMTP caído — sin que el otro se entere jamás.

Los dos canales se activan por separado, evento a evento, en **Settings →
Notifications** — una persona puede conservar la fila en la aplicación para
las aprobaciones y apagar su correo, o al revés.

Las solicitudes para compartir páginas y los cambios de propietario nombran la página y la persona, nunca sus datos.

Un run `eval` es uno de los muchos que la evaluación comunica juntos; no envía una notificación individual de finalización o fallo.

La misma página lleva además
cada uno de los demás eventos que entrega el buzón: un run que termina o falla
desatendido - el final de una rutina con su nombre y el comienzo de su
respuesta, para quien la configuró -, la petición de activar una capability,
para el propietario del agent, la ingesta de un documento que **falla**, la cifra propia de un
ciclo completo de sincronización de un conector, y el propio
anuncio de un app admin - `POST /admin/announcements`, todavía sin página en
la consola - dirigido por organización y, opcionalmente, por rol, y
restringido a uno o ambos canales.

Llegan también las propuestas que el [trabajo por iniciativa
propia](#proactive-work) hizo para una persona.

Un documento que se indexó limpiamente no escribe nada, y es deliberado. Antes
escribía una fila cada uno, lo que en el caso corriente — una carpeta de
archivos añadidos de golpe — significaba una notificación por archivo diciendo
que nada había ido mal, sepultando las que sí había que leer. Donde se informa
ahora de una ingesta corriente es en el propio estado del documento dentro de
su colección y, para un ciclo de conector, en la única línea que escribe
`sync_completed` cuando termina el intento entero. El fallo sigue llegando a
quien subió el archivo, o a los administradores de la organización cuando no lo
subió nadie.

La única excepción son los informes de uso semanales y mensuales configurados
en el agent, más abajo: ambos comparten una única preferencia de correo
heredada, así que apagar el correo de un informe apaga también el del otro —
la fila en la aplicación de cada uno se sigue activando por separado.

A diferencia de todo lo de arriba, un evento de seguridad o un cambio de
configuración no se puede apagar en ninguno de los dos canales. Llega a los
owners y admins de esa misma organización — no a la audiencia `admins` más
amplia de arriba, y nunca a los app admins del deployment, salvo que la propia
acción no tenga organización a la que atribuirla, en cuyo caso la recibe cada
app admin. Nada de esto ensancha lo que documenta esta página: es
el mismo buzón donde aterrizan las alertas configuradas en el agent de
arriba, y la regla de exclusión de abajo se sigue aplicando a todo lo que se
puede apagar.

Pero no sin límite: cada uno está limitado a veinte escrituras por minuto por
actor y tipo de evento, así que una cuenta que hace cambios rápidos no puede
inundar a cada admin — la entrada de audit detrás de cada uno se registra de
todos modos, en el propio trail ([Audit](#audit)), sin importar si la
notificación sobrevivió al límite.

Pasado ese tope la bandeja no se queda callada. En lugar del resto llega un
aviso por actor y por minuto, que dice que el minuto ha tenido más movimiento
del que la bandeja puede listar y que todos esos eventos están en el trail. Eso
importa porque estos dos no se pueden desactivar: un actor podría gastar el
margen en veinte ediciones inofensivas y hacer después lo único que merece la
pena vigilar, sin que nada lo dijera. El aviso no lleva un recuento — contar
significaría reescribirlo en cada evento posterior, que es justo la inundación
que el tope existe para evitar.

Una fila se retira del buzón noventa días después de escribirse si está
_leída_, y un año después sin importar si llegó a abrirse — contando siempre
desde que se escribió, nunca desde que se leyó, así que una fila abierta el
día antes de su límite superior desaparece junto con cualquier otra de esa
edad. Un barrido en segundo plano, no algo que dispare una persona. El barrido
de correo no envía una fila que ya lo haya superado, así que un worker que se
recupera de una caída larga no puede mandar una notificación camino de su
borrado.

[Limpiar](console.md#the-bell) es lo otro, y no lo mismo: una persona saca una
fila de su propia lista al instante, y la fila en sí se conserva hasta que este
barrido llega a ella.

Lo que sobrevive a eso depende de sobre qué era el aviso. Un evento de
seguridad, un cambio de configuración y el anuncio propio de un admin empiezan
como una entrada de auditoría, y esa entrada sobrevive a la fila que mostró el
buzón ([Auditoría](#audit)). El desenlace de un run, un resultado de ingestión
y un informe de uso no escriben entrada de auditoría propia: el run, el
documento y el gasto que describen son el registro, y el aviso solo es cómo se
enteró alguien — las cifras calculadas de un informe periódico, que no viven en
ningún sitio salvo en la notificación, son lo único que una fila caducada se
lleva consigo.

### Un resultado, cerrado una vez { #one-outcome }

Una aprobación en espera, una ejecución fallida y un hallazgo de una vigilancia son cosas que alguien
tiene que atender, así que cada una es un **resultado**: un elemento por persona y causa, sin importar
cuántos lugares lo cuenten (#2575). La tarjeta sobre el chat de donde viene, la fila de la campana, el
push y el correo son entregas de ese único resultado, no cuatro elementos.

Un resultado se confirma, se resuelve o se silencia una vez, desde la tarjeta del chat o la campana, y
cada lugar lo sigue. Confirmar lo marca como leído y cancela el push y el correo que aún esperan para
salir; resolver y silenciar además lo despejan. Una entrega que un worker ya tiene se revisa de nuevo
justo antes de enviarse, así que la cerrada entre tanto no sale. `POST /outcomes/{id}/settle` recibe
la acción y `GET /outcomes` lista los resultados propios de quien llama, opcionalmente de un chat.

Decidir la última aprobación pendiente de una ejecución resuelve su resultado. Una causa silenciada sigue silenciada. En otras notificaciones, una causa resuelta o confirmada puede reabrirse cuando vuelve; un hallazgo repetido conserva su estado anterior.

Cada hallazgo de una vigilancia, incluso sin aviso urgente en la campana, comparte un caso con su fila de Inbox y su tarjeta en el chat de origen. **Hecho** resuelve ese caso; descartar el hallazgo lo silencia. Ambas acciones cancelan las entregas externas pendientes. Confirmar detiene los avisos y los futuros anuncios del informe diario, pero mantiene el hallazgo pendiente en Inbox con el estado **Confirmado**. Eliminar la vigilancia resuelve sus casos pendientes antes de borrar los hallazgos. Las decisiones afectan solo a la persona propietaria dentro de la misma organización.

Solo **Restaurar** explícitamente un hallazgo descartado reabre el mismo caso. No repite los avisos ni las entregas anteriores. La migración `0264_finding_outcome_state.py` vincula los hallazgos existentes con sus casos, crea casos para los no urgentes, conserva identificadores y decisiones anteriores de Inbox y cancela entregas obsoletas pendientes. Un downgrade elimina el vínculo, pero no revierte decisiones ni recupera mensajes aceptados por el proveedor; conserve una copia anterior a la actualización.

### Configurado en el agent { #configured-on-the-agent }

Quién se entera de un agent forma parte del spec del agent, bajo **Limits → Alerts**.
Una audiencia para todo el deployment convertía al agent ruidoso y al que nadie puede
perderse en el mismo ajuste, así que la única forma de callar al primero era quedarse
sordo al segundo.

| Alerta | Se dispara cuando | Audiencia por defecto |
| --- | --- | --- |
| **Budget** | este agent alcanzó su propio tope mensual | los admins y el dueño del agent |
| **Approvals** | una llamada a herramienta se aparcó | quien inició el run, más los admins |
| **Usage** | semanal y mensual, lo que gastó este agent | apagada |

El log de un workflow tiene alertas propias, que se fijan en la página Logs del workflow
y no en una spec; ver [Alertas](workflows.md#log-alerts). Se disparan cuando las líneas
coincidentes de una regla alcanzan su umbral dentro de su ventana, y van al propietario
del workflow salvo que la regla diga otra cosa. Siguen las mismas preferencias de campana
y correo que el resto de esta sección, en **Workflow log alerts**.

Una audiencia es una lista de roles, no de direcciones:

| Audiencia | Se resuelve a |
| --- | --- |
| `admins` | los owners y admins de la organización, **más los app admins del deployment** |
| `owner` | el dueño del agent |
| `initiator` | quien inició el run; nadie, para un run que empezó un horario |
| `chosen` | exactamente los miembros nombrados junto a ella |

Roles y no direcciones porque un spec se exporta al repositorio de un cliente y
sobrevive a las personas que hay en él: `admins` sigue significando las personas
correctas tras una reorganización, y las significa en la organización a la que se
importe el spec. Un miembro nombrado que se haya ido no aporta nada en vez de lanzar
un error — una cola de aprobaciones no debe quedarse muda porque un id ya no resuelva.

### Una alerta enlaza a donde está la decisión { #an-alert-links-to-where-the-decision-is }

**El correo de aprobaciones abre la cola** — `/runs?tab=approvals`, la pestaña
Approvals de Activity, que es la única superficie que lleva Approve y Reject. Antes
abría `/agents/{id}`, el Builder: una frase de prosa sobre llamadas a herramientas
que llegan a una cola, y ninguna cola. Así que el único correo cuyo propósito entero
es _alguien tiene que decidir, ya_ aterrizaba a una búsqueda de la decisión, mientras
el run aparcado envejecía hacia `ApprovalService.expire_stale` (#935). No hubo URL
para la pestaña hasta que la #934 la puso en `?tab=`.

Deliberadamente **no** nombra el run con `&run=`, aunque la alerta lleve uno: los
controles de decisión están en la fila de la cola, y por debajo de `lg` un run
enfocado sustituye a la lista — lo que los ocultaría justo a quien más probablemente
esté en un móvil.

El correo de budget abre el agent, y ese es el destino correcto para él: el tope del
que informa se edita ahí.

### La alerta de aprobación son dos correos { #the-approval-alert-is-two-emails }

`approvals:decide` pertenece a `owner`, `admin` y `operator`. La audiencia por defecto
de una llamada aparcada incluye a quien inició el run, y un builder que inicia su
propio agent desde el chat es el iniciador corriente — así que la alerta llegaba
habitualmente a alguien a quien la plataforma iba a rechazar. Recibían **"waiting on
your approval"** con un botón **Review the request**, lo seguían, y Activity no
dibujaba pestaña Approvals alguna: el rechazo llegando como una pestaña ausente en vez
de como una frase.

Así que la audiencia se divide por el permiso en lugar de recortarse a él:

| Quien recibe tiene | Recibe |
| --- | --- |
| `approvals:decide` | la solicitud, con el enlace a la cola |
| cualquier cosa menor | el _hecho_: el run está retenido y no fallido, aprobarlo corresponde a un owner, admin u operator, y no se le pide nada |

Recortarla dejaría a la única persona que seguro espera el run — quien lo inició — sin
oír nada sobre por qué se detuvo.

El segundo correo **no lleva enlace**, y eso es deliberado, no algo a medias. Que
`agents:view` sea un permiso de rol no hace alcanzable un agent: el acceso se resuelve
por recurso, así que un destinatario `chosen` sin grant sobre un agent privado
recibiría una segunda llamada a la acción que la plataforma rechaza — el mismo defecto
en otro sitio. Nada se le pide a quien lee, así que nada se le ofrece. Tampoco afirma
que se haya avisado a otros: una audiencia de una sola persona sin capacidad de
decidir significa que a nadie que pueda decidir se le escribió, y prometer lo
contrario la dejaría esperando a alguien que nunca se enteró.

Qué roles deciden se lee del catálogo de permisos, no se lista al lado: un rol que gane
o pierda `approvals:decide` no debe dejar atrás el enrutado, que es el mismo defecto un
nivel más arriba.

### Cada enlace dice de qué organización trata { #every-link-says-which-organization-it-is-about }

La consola actúa sobre la organización que quien lee usara por última vez: `apiClient`
estampa `X-Organization-Id` desde una selección guardada por navegador. Antes ninguna
URL de alerta llevaba ninguna, así que alguien en dos organizaciones que hubiera
trabajado por última vez en Globex abría la alerta de aprobación de un run de Acme y
leía la cola **de Globex** — muy probablemente vacía, y leyéndose como _nada espera_
sobre un run que está aparcado y envejeciendo hacia `ApprovalService.expire_stale`.
Los enlaces a agents fallaban más calladamente: `/agents/{id}` bajo la organización
equivocada es un rechazo para un agent que quien lee sí puede ver, a un cambio de
distancia.

Así que cada enlace lleva `org=<id>`, construido en un solo sitio —
`NotificationService._link`, que elige el separador según la ruta porque el
enlace de aprobaciones ya lleva `?tab=approvals` — y no en las cuatro llamadas.
La consola lo adopta igual que el id de `/orgs/{id}`: una página que nombra una
organización _es_ esa organización. La adopción es un layout effect en
`ActiveOrgGuard`, antes del reinicio de la caché de tenant y de las consultas de
la página, así que su primera petición ya lleva el tenant correcto. La ruta manda
sobre el parámetro: un enlace dice de qué organización trataba una alerta, y no
saca a nadie de la página en la que está.

A quien haya dejado esa organización desde entonces se le dice, en vez de moverlo en
silencio: el rechazo nombra el enlace como la razón, porque que te cambien sin decir
nada es como la página de otra organización acaba siendo la respuesta a la alerta. No
puede nombrar la organización — no son miembros, así que no está en su lista.

### Dos reglas que no se negocian { #two-rules-that-are-not-negotiable }

**Una exclusión por persona solo resta.** Los interruptores propios de cada
destinatario en **Settings → Notifications** se aplican los últimos. Un agent puede
decidir que los admins deberían enterarse de él; un admin puede seguir decidiendo que
no quiere correo de budget. Nada de lo que escriba el autor de un agent recluta a
nadie en una bandeja de entrada.

**El tope de la organización ignora el spec por completo.** Ese límite detiene a cada
agent de la organización y el autor de un agent no puede subirlo, así que su alerta va
a los administradores pida lo que pida cualquier agent. Un agent no puede silenciar un
límite que no controla.

### El silencio significa algo { #silence-is-meaningful }

Una organización que no ejecutó nada no recibe informe. Un "0 runs, $0.00" semanal es
el informe que la gente filtra a una carpeta, y entonces el que importaba va ahí
también.

La cifra de ese informe es el gasto de la organización en la ventana — la misma
aritmética con la que se aplica el tope, ingesta incluida y runs delegados contados
una vez. Un informe cuyo total discrepa del límite que la plataforma aplica es peor
que ningún informe, porque los dos números parecen autoritativos.

El envío nunca bloquea y nunca lanza hacia quien llama: un run que ya ha terminado no
debe fallar otra vez porque SMTP estuviera caído.

## Evaluación de pago { #ai-assistance }

Calificar, generar casos y mejorar son acciones de pago explícitas. El dataset posee sus modelos e instrucciones: **Grading** configura el calificador; **Settings**, la generación y mejora. La consola muestra una estimación antes del inicio.

No hay interruptor organizacional de asistencia de IA ni modelo organizacional que sustituya al del dataset. El precio no elige un modelo supuestamente más capaz. Un test set creado automáticamente empieza con el modelo del agent; elegir un calificador independiente es una decisión explícita del dataset.

Se mantienen los permisos, credenciales, budgets de la organización y límites de runs. Cada llamada se mide y registra en Activity. Los trabajos en cola conservan su configuración inicial; los cambios afectan a trabajo posterior. Las filas históricas de ayudantes se conservan para atribución y control de budgets.

## Excepciones de promoción { #promotion-overrides }

Un entorno [guardado por suites de evals](environments.md#guarding-a-promotion-with-evals)
acepta una versión solo cuando pasan en ella. Promocionar por encima de ellas es
una **excepción**: necesita `promotions:override` - lo tienen owners y admins, y
se puede dar a un rol propio - de una persona que ha iniciado sesión como ella
misma, no de una clave de API ni de un administrador que la suplanta, y un
motivo de al menos diez caracteres. Se registra en la
[auditoría](#audit) como `agent.environment_promotion_overridden`, con el motivo
y el resultado de cada guardiana, se muestra en la versión del historial y se
envía al propietario de cada guardiana que no pasó.

Cambiar qué suites guardan un entorno, mover la fijación de una guardiana o
debilitar lo que comprueba una guardiana necesita el mismo permiso; cada uno de
estos cambios, y un umbral más estricto, se registra como
`agent.environment_guards_changed`. Un cambio en la puntuación de un dataset se
registra como `dataset.scoring_changed`, con los nombres de las partes que
cambiaron en lugar de su contenido, y la creación de su juez como
`dataset.judge_created`.

## Auditoría { #audit }

Las acciones que cambian accesos o gastan dinero se registran con un actor, y un
contexto sin sujeto **lanza** en vez de dejar viajar la ausencia. Así que una entrada
que no nombra a nadie significa exactamente dos cosas, y la `action` dice cuál: el
barrido de caducidad de aprobaciones, y un comando de operador en la shell del
deployment.

Enlazar una credencial a una colección es una de esas acciones. Las entradas
`sync_source` registran crear, clonar, reapuntar y borrar una fuente, porque la fila
decide quién acaba pudiendo leer lo que ingiere
([Procesamiento de archivos](file-processing.md#who-ends-up-able-to-read-what-a-source-ingested)).

Una **lectura masiva** privilegiada también se registra. Cada exportación a CSV
escribe una entrada `runs.export`, `approvals.export` o `spend.export` nombrando la
ventana y el número de filas — quién se llevó la tabla entera de la pantalla es una
pregunta barata de responder ahora e imposible de reconstruir después.

La escritura comparte la transacción de la petición que actúa, así que falla cerrando:
una entrada que no se puede registrar revierte la acción que describe en vez de dejar
aterrizar sin auditar una mutación privilegiada.

`audit:read` protege su lectura. Que un app admin se salte las reglas es exactamente
lo que el rastro existe para exigir cuentas.

El rastro también se mantiene honesto consigo mismo. Cada entrada se suma a una
cadena de hashes por organización — lleva un hash sobre su propio contenido con el
hash de la entrada anterior entretejido — de modo que editar, reordenar o insertar
una entrada, o borrar una del medio, hace divergir todos los hashes posteriores.

`agenticos cmd audit-verify` recorre cada cadena, recalcula los hashes y nombra la
primera entrada que ya no coincide; sin argumento comprueba todas las cadenas,
incluida la de todo el deployment, que guarda acciones sin organización — un cambio
en los ajustes del deployment, una suplantación, la gestión de usuarios del app
admin — y termina con un código distinto de cero si alguna cadena falla. Esto es
**detección, no prevención** — un operador con la base de datos aún puede reescribir
una fila y recalcular todos los hashes posteriores — así que una ejecución limpia es
evidencia de que nadie manipuló sin reforjar también la cadena, no prueba de que las
filas sean inmutables.

Hay dos borrados que la cadena no puede detectar por sí sola, porque las filas que
quedan siguen siendo internamente consistentes: recortar las entradas más nuevas de
una cadena y borrar por completo la cadena de una organización. En su lugar los
detecta un **checkpoint** — una marca de nivel máximo por organización que
`record_audit` avanza junto a cada entrada, bajo un trigger de base de datos que le
rechaza retroceder o ser borrado. `audit-verify` señala una cadena cuya cabeza está
por detrás de su checkpoint, o un checkpoint cuya cadena ha desaparecido.

Conviene decir con precisión qué cubre ese trigger, porque es fácil leer de más.
Cierra el camino de escritura ordinario — un administrador de la aplicación actuando
a través del producto, y un error en este código — que es el modelo de amenazas para
el que está escrito este rastro.

No es un control frente a quien tenga las credenciales de la propia base de datos. La
aplicación y sus migraciones se conectan con el mismo rol, y ese rol es propietario de
la tabla de checkpoints: puede eliminar el trigger, y un `TRUNCATE` vacía la tabla sin
llegar a disparar un trigger de borrado por fila. Un superusuario puede hacer ambas
cosas. Cerrarlo requiere la marca de nivel máximo guardada donde los roles de esta
base no alcanzan — un almacén de solo anexado o con bloqueo de objetos fuera de ella
—, lo que sigue siendo un trabajo posterior planificado.

Dos escrituras auditadas para una misma organización no pueden bifurcar la cadena:
cada una añade bajo un bloqueo por organización, así que se serializan en una sola
línea en vez de extender ambas la misma cabeza.

Una acción **suplantada** nombra a ambos. Cuando un app admin actúa como otra cuenta,
el token de acceso lleva al administrador como claim `act`; cada entrada que registre
esa petición mantiene `actor_user_id` como la cuenta en cuyo nombre se actúa y añade
`impersonator_user_id`, el administrador detrás. Así "quién leyó la conversación de
este cliente" resuelve a una persona aunque la acción se registrara como propia del
cliente. Es nulo en una petición corriente, donde nadie actúa como nadie, y no se
rellena hacia atrás: si una acción pasada fue suplantada no se puede saber a
posteriori, e inventar una respuesta sería una acusación falsa y no una ausencia.

La suplantación misma se registra por ambos extremos. `admin.user.impersonate` nombra
la cuenta, la fila de sesión que es la suplantación, y cuándo caduca;
`admin.user.impersonation_ended` nombra esa misma sesión cuando el administrador la
termina, con el administrador como actor. Una caducidad no escribe nada, porque nadie
actuó — y tampoco lo hace quien cierra sesión en todas partes, que termina una
suplantación con `DELETE /sessions` igual que termina cualquier otra sesión. Si a la
persona se le _dice_ es el ajuste `notify_impersonated_users` del deployment
([El deployment](deployment.md#acting-as-another-account)); apagado, que es lo normal,
este rastro es el único registro.

Una suplantación no puede **enlazar una identidad externa** a la cuenta en cuyo nombre
actúa. Confirmar el enlace de una cuenta de chat y completar el OAuth de una
integración fijan ambos una identidad a quien sea la petición, y bajo una suplantación
eso es la cuenta objetivo — así que la cuenta de Telegram o el grant de OAuth del
propio administrador quedarían pegados a la cuenta de otra persona y sobrevivirían a
la hora que acota la suplantación. Ambas se rechazan con un 403 mientras se suplanta,
porque un administrador reparando la conexión de un miembro no es un flujo que esta
plataforma tenga. El miembro enlaza sus propias cuentas, como él mismo.

La línea es cualquier credencial fijada al miembro, no solo una identidad: además
de la cuenta de chat y del grant de OAuth de arriba, un bearer token tecleado en
la conexión del miembro también se rechaza. Ese token no es la identidad del
administrador, pero sellado bajo el ámbito de vault del miembro habla como el
dueño de esa cuenta para cada agent suyo, sobrevive a la hora que acota la
suplantación y queda registrado contra el miembro. Lo que un administrador sigue
haciendo por él es configuración sin secreto alguno — un nombre, una URL, una
lista de herramientas — y borrar un token guardado, que no conserva nada.

La conexión compartida de una **organización** es igual, aunque esté pensada para que
la introduzca un admin: un bearer token tecleado en una bajo una suplantación también
se rechaza, porque sellado bajo el ámbito de vault de la organización habla como la
cuenta propia del administrador para cada agent que enlace la organización, pasada la
hora en que la suplantación termina. Crear una conexión de organización ya registraba
al administrador detrás; actualizar una no registraba nada, así que un token rotado
sobre una conexión existente deja ahora el mismo rastro (#1521).

## Retención { #retention }

Hasta #1420 nada se borraba de forma programada. Las conversaciones, sus
archivos, las filas de runs y sus manifiestos, los workspaces, la memoria de un
agente, los documentos subidos y las entradas de auditoría vivían hasta que
alguien borraba la organización. Eso es un problema de protección de datos en un
sentido y —para la auditoría— de cumplimiento en el otro: HIPAA §164.316(b)(2)
quiere conservar un registro de auditoría seis años, y el RGPD quiere minimizar
todo lo demás. Por eso el periodo es **por clase**, y ambas obligaciones tienen
su ajuste.

Se configura en **Organizaciones → un workspace → Miembros → Retención**,
protegido por `org:settings`. Un barrido corre una vez al día y **borra de
verdad**: una política que conservara las filas no sería una política.

Los datos de [Virtual Tables](virtual-tables.md#limits-and-retention) entran en el mismo
barrido, con plazos de todo el deployment que ninguna organización fija: los receipts de
idempotencia a las 24 horas, las filas de outbox despachadas a los 3 días y el history de
un registro a los 365 días, que es también lo que elimina el history de un registro
borrado. La entrada que escribe nombra `table_receipts`, `table_outbox` o `table_history` y
un recuento.

Los tres números propios del despliegue —`retention_defaults`,
`retention_max_days` y `audit_retention_floor_days`— son campos de los ajustes
del despliegue, escritos por una app admin con `PATCH /admin/deployment-settings`
como cualquier otro ajuste de ahí. Todavía no hay formulario en la consola para
ellos; la página de la organización es donde se fijan los periodos por tenant.

### Las clases { #the-classes }

| Clase | Qué se va con ella | Medido desde |
| --- | --- | --- |
| Conversaciones | Mensajes, llamadas a herramientas y archivos; un lote bloqueado excluye conversaciones con runs sin terminar, y la tarea duradera de borrar archivos se confirma junto con las filas | La última actividad del hilo, para que uno al que alguien vuelve no sea viejo |
| Runs | La fila del run, su manifiesto y sus aprobaciones de herramientas | El inicio del run |
| Workspaces | El registro que la plataforma tiene de los archivos de un agente. Con el backend `state` la fila _es_ el almacenamiento; los archivos de un backend de sandbox los recoge el TTL del propio sandbox. Los de un proyecto se conservan mientras exista el proyecto | El último uso |
| Aplicaciones | Una [página publicada](artifacts.md), todas sus versiones, grants y enlace público; el borrado de archivos se encola en la misma transacción | La última publicación, para que un informe que una programación vuelve a publicar no sea viejo |
| Memoria | Los archivos de memoria de un agente | La última escritura, porque una nota se escribe una vez y se lee durante meses |
| Documentos subidos | La fila, sus vectores y el archivo subido | El momento de la subida |
| Auditoría | Entradas en el rastro de esta organización | El momento de la entrada |

**Un documento que todavía se está ingiriendo tampoco.** Lo tiene un worker, y
quitarle la fila y el original subido por debajo de una ingestión que después
escribe vectores deja contenido buscable que ningún barrido posterior puede
nombrar. Solo se retiran las filas terminadas y las fallidas.

**Un documento que sincronizó un conector no se barre.** Su vida pertenece a la
fuente que lo puso ahí: borrarlo aquí eliminaría una fila que la siguiente
sincronización `new_only` vuelve a crear desde el mismo archivo sin cambios,
quemando gasto de embeddings para nada. Lo que se barre es lo que alguien subió,
cuya vida no pertenece a nada más.

### Qué número gana { #which-number-wins }

Tres capas, resueltas en `app/core/retention.py` y en ningún otro sitio:

1. **El valor por defecto del despliegue**, para una organización que no ha dicho
   nada. Ausente significa para siempre: una plataforma que al actualizarse
   empezara a borrar el historial de una instalación existente sería una en la que
   nadie confiaría para la siguiente actualización.
2. **El periodo propio de la organización**, más corto o más largo.
3. **El techo del despliegue**: nada de esta clase vive aquí más de N días, y una
   organización no puede levantarlo.

La auditoría va al revés. El despliegue fija un **suelo** —lo mínimo que puede
vivir una entrada, seis años mientras nadie lo cambie— y una organización puede
alargarlo y nunca acortarlo. **La auditoría todavía no se barre**: el periodo se
resuelve y se informa, y una organización queda sujeta al suelo, pero no se borra
ninguna entrada, porque la cadena de hashes y su checkpoint append-only se apoyan
en que las entradas no se van a ninguna parte, y un borrado a secas hace que
`audit-verify` informe la retirada como manipulación.

Retirar una cadena y poder seguir demostrando que el resto está intacto es un
mecanismo que aquí todavía no existe, así que el suelo se mantiene y el barrido
deja la auditoría en paz.

Un techo sigue aplicándose a la auditoría donde ambos no se contradicen. Donde sí,
gana el suelo y la contradicción se informa. Un rastro que una administradora puede acortar no es
un rastro, así que un periodo por debajo del suelo se **rechaza** en lugar de
subirse en silencio: conservar entradas más tiempo del que dice el número en
pantalla es un error de su propia clase.

Un suelo por encima de un techo es una contradicción, y se informa en lugar de
resolverse. La página de ajustes nombra la clase; una administradora decide cuál
se aplica. Elegir una dejaría un despliegue comportándose de forma distinta a su
propia página de ajustes.

### Qué sobrevive a un borrado { #what-survives-a-purge }

**La factura.** El gasto de un mes es una suma sobre `agent_runs`, así que
borrarlos de verdad haría caer a cero el acumulado del mes de una organización
según pasara la ventana, y un límite medido sobre esa cifra dejaría de aplicarse
el resto del mes. El barrido lee lo que costaron los runs que expiran antes de
borrarlos y guarda un total por organización y mes en `purged_run_spend` —un
número y un recuento, sin agente, sin modelo, sin el nombre de nadie—.
`app/services/spend.py` lo suma al total en vivo, y ese es el único sitio donde
ambos se encuentran.

**El rastro del propio barrido.** Una entrada por organización y barrido, que
nombra la clase y el recuento y nada más: una entrada de auditoría que citara lo
que borró conservaría el contenido más allá de la retención que lo quitó.

### Cuando falla { #when-it-fails }

Por clase, no por barrido. Un almacén de vectores caído no puede impedir que se
borren conversaciones, así que se intenta cada clase, su fallo se registra y se
nombra en la entrada de auditoría, y el barrido sigue. La siguiente pasada lo
reintenta, porque un lote que no quitó nada simplemente vuelve.

Los borrados van en lotes de 500, hasta cuarenta pasadas por clase y barrido. Una
organización que llega a una política de noventa días tras dos años se quita el
atraso a lo largo de varios días, en vez de en un barrido que retiene un bloqueo
durante una hora con todos los demás flujos periódicos haciendo cola detrás.

Cada clase usa un savepoint: un error SQL revierte sus cambios y tareas de
limpieza sin abortar las clases siguientes. Las tareas duraderas borran los
archivos tras el commit. Los fallos del almacenamiento y los reintentos aparecen
en Background jobs. Revertir el borrado conserva los archivos intactos.

### Adónde no llega { #what-this-does-not-reach }

- **A las copias de seguridad.** Un borrado quita filas y archivos del despliegue
  en vivo. Lo que guarde tu calendario de copias es tuyo de caducar, y una
  restauración devuelve lo que contuviera la instantánea.
- **A lo que enviaste a otro sitio.** Los logs mandados a un colector externo, las
  trazas en un proyecto de observabilidad alojado y cualquier cosa que retenga un
  proveedor de modelos se rigen por los ajustes de esos servicios, no por este.
- **Al control por entrada de memoria**, que es [#1594](https://github.com/vstorm-co/agenticos/issues/1594),
  ni a borrar a una persona en toda una organización, que es
  [#1421](https://github.com/vstorm-co/agenticos/issues/1421). Esta página trata
  de la edad; aquellas tratan de un sujeto.

## Lo que nada de esto cubre { #what-none-of-this-covers }

Vale la pena decirlo, porque una página de governance da a entender lo contrario:

- **No hay limitación de tasa por agent.** Los límites que existen están en las
  superficies públicas — el widget de embed mide mensajes por visitante, un bot de
  canal mide a cada remitente ([Canales](channels.md)) — no en el deployment: no hay
  un budget de peticiones por agent, y las rutas propias de la consola no se miden.
- **No hay filtrado de contenido.** Lo que dice un agent es lo que dijo el modelo.
- **No hay control de salida sobre MCP.** Un servidor enlazado se alcanza por la red
  desde el worker; restringir adónde puede ir eso es configuración del deployment, no
  un ajuste de aquí.

## Resumen { #recap }

- **Dos topes, y no se pueden fundir.** Un budget acota el dinero; un límite de pasos
  acota un bucle que es barato por llamada y no termina nunca.
- El budget se comprueba **antes** de cada petición al modelo, y un run fallido
  registra igualmente lo que gastó.
- El tope es un techo sobre el gasto **confirmado**. Los runs concurrentes no se ven
  entre sí, así que un tope estricto significa una sola cola.
- Una **aprobación se decide una vez**, y una segunda decisión sobre una aprobación ya
  resuelta se rechaza.
- Leer un run está **autorizado, no en propiedad** — `runs:view` lee el run de un
  colega, y el de otro tenant se lee como ausente.
- **El silencio significa algo.** Una alerta que no llegó significa que la cosa no
  ocurrió, lo cual solo es cierto porque aquí nada es de mejor esfuerzo.
- **Una promoción por encima de sus evals es una excepción**, con un motivo, un
  permiso propio y un registro en la versión.

## Referencias { #reference }

- [Conceptos](concepts.md) — spec, versión, exposición, run.
- [Permisos](permissions.md) — quién puede fijar todo esto.
- [Configuración](configuration.md) — los ajustes a nivel de deployment.

## Pausa automática por fallos repetidos { #routine-health }

Las rutinas y observaciones se pausan por defecto tras **tres comprobaciones fallidas consecutivas**. El formulario permite elegir un límite de 1 a 20 o desactivarlo. Cuentan los fallos del proveedor o de ejecución, las lecturas inválidas y las fuentes inaccesibles. No cuentan la espera de aprobación, un ordenador desconectado ni los fallos de entrega; una comprobación vacía pero verificada como correcta reinicia la serie. Los demás bloqueos de permisos y presupuesto siguen aplicándose aunque se desactive este límite.

Cada activación tiene una única observación persistente. Los reintentos no aumentan el contador y los resultados tardíos se evalúan según el orden de activación. La pausa automática detiene futuras activaciones; una solicitud al modelo ya iniciada puede terminar.

El incidente existente del propietario registra la pausa y su notificación usa las preferencias y rutas compartidas. Un incidente sin confirmar recibe un único aviso de pausa que sustituye su entrada anterior en Inbox. Reanudar o cambiar la política invalida los avisos de pausa pendientes. Confirmar el incidente no reinicia el estado de ejecución ni reanuda la rutina. Tras corregir el problema, reanúdela expresamente para iniciar una nueva ventana de fallos. Cambiar el límite también inicia una ventana nueva.

La lista muestra la pausa automática y el número de fallos. Se conservan como máximo 50 observaciones por rutina, sin cuerpos de fuentes ni errores del proveedor. Se incluyen en la exportación personal con límite de tamaño y se eliminan en cascada con el propietario o la rutina. La migración `0275_routine_health.py` activa el valor predeterminado también para las rutinas existentes: actualice API y workers juntos. Revertir elimina los metadatos y la política, pero conserva la pausa.

## Fuentes revisadas de las rutinas { #routine-sources }

Al crear una rutina se fija la configuración publicada de sus fuentes, las cuentas MCP concretas, las generaciones de consentimiento y los nombres de herramientas conocidos. La revisión incluye delegados fijados, colecciones de conocimiento, habilidades, contexto y configuración de capacidades; las inscripciones A2A remotas conservan también la vinculación de endpoint, tarjeta y credencial. Los cambios de instrucciones o ajustes del modelo pueden seguir una publicación. Añadir fuentes, cambiar cuentas o renovar su consentimiento requiere que el propietario revise el panel de fuentes en el diálogo de consentimientos. Esta revisión no elimina las aprobaciones de acciones ni concede acceso a recursos. Siguen aplicándose los permisos actuales.

Las activaciones en cola, las esperas de dispositivos y las ejecuciones aparcadas conservan la identidad de su revisión original. Una revisión posterior no amplía los permisos del trabajo anterior. El sondeo del buzón comprueba la cuenta de entrada antes de leer; los eventos conservan el consentimiento admitido antes de esa lectura. Las llamadas MCP usan alias revisados y excluyen herramientas no aprobadas. Las fuentes revisadas no disponibles provocan un rechazo explícito. Una revisión necesaria antes de ejecutar aparece en el caso de atención existente del propietario y no cuenta como un fallo del modelo.

La migración `0276_routine_sources.py` no concede consentimiento a rutinas existentes. Drena los workers antiguos, actualiza API y workers conjuntamente y revisa explícitamente las rutinas antiguas antes de ejecutarlas. Solo el propietario en la consola puede sustituir el consentimiento; no pueden hacerlo las claves API, la suplantación ni los agentes desatendidos.

Las rutinas con presupuesto comparten una clave de concurrencia de la cola para evitar que eventos simultáneos admitan varios modelos sobre el mismo gasto completado. La extracción de memoria también se ejecuta en serie por organización y rol de mantenimiento. Cada trabajo comprueba el presupuesto actual; una llamada ya admitida todavía puede terminar por encima del límite.

Las alertas de trabajos pausados de todo el despliegue conservan el estado normal de lectura y descarte de notificaciones; no se presentan como casos de una organización.

### Rutinas, conversaciones de trabajo e Inbox { #routines-working-conversations-and-inbox }

**Routines** reúne tareas programadas, tareas por eventos y plantillas de
supervisión de solo lectura. Un trigger determina _cuándo_ se ejecuta la rutina;
la plantilla de supervisión limita _qué_ puede hacer. Use una rutina por
responsabilidad, sin duplicar una observación y una tarea programada para el mismo buzón.

Elija **Después de cada ejecución completada** (`result_mode: report`, el valor
predeterminado existente) para informes, o **Solo cuando algo necesita atención**
(`result_mode: inbox`) para comprobaciones silenciosas. El agente devuelve elementos
relevantes con identificadores estables de elemento y revisión, contexto y el
borrador completo solicitado. Un resultado vacío no crea conversaciones ni avisos
de finalización. Cada elemento tiene una conversación privada y un caso en Inbox.
Las revisiones repetidas se deduplican de forma persistente, incluso tras resolver
el caso. Una revisión nueva actualiza la misma conversación y reabre el caso;
un elemento silenciado permanece silenciado.

Abra la conversación desde Inbox para editar el borrador o solicitar el siguiente
paso. Leer o editar no envía nada ni completa el caso. Siguen vigentes los permisos
y las aprobaciones de herramientas. Las herramientas MCP de plataforma
`list_inbox_cases` y `settle_inbox_case` permiten encontrar el caso y resolverlo
tras completar correctamente el trabajo solicitado o recibir una confirmación
explícita. Inbox y chat comparten estado; al resolver se cancelan notificaciones
pendientes. La supervisión de solo lectura puede preparar texto en la conversación,
pero no guardar ni enviar borradores mediante el servicio de origen.

Las sugerencias nocturnas siguen siendo un análisis opcional e independiente de
solicitudes recientes y memoria, no otro escaneo del buzón. La supervisión usa el
modelo de su agente seleccionado y no requiere un modelo de sugerencias. La
organización controla disponibilidad, frecuencia y presupuesto compartido por
persona para trabajo en segundo plano. Las rutinas ordinarias conservan los límites
del agente, de la organización y su límite propio opcional. Días y hora también
controlan el resumen de hallazgos no urgentes.

El resumen matinal enumera hallazgos no urgentes ya guardados sin otra llamada
al modelo ni otro escaneo de fuentes. Es una notificación, no una segunda rutina.
Las herramientas MCP de Inbox sirven a sesiones iniciadas y credenciales de
confianza de un chat privado; estas últimas solo acceden a casos de ese chat.
Las claves API ordinarias, ejecuciones desatendidas e identidades de respaldo del
publicador no pueden leer ni resolver casos privados de Inbox.