---
title: "Esperas: lease frente a deadline"
description: "Un deadline hace dos oficios a la vez —detectar el fallo y repartir el recurso— y confundirlos hace que un tool lento y un tool muerto acaben igual. Qué separó ALLAN tras el incidente del 18-07-2026, qué renovó, qué mató y qué sigue sombreado por un timeout duro del mismo valor."
allan.topic: foundation
allan.service: allan-server
allan.date: 08/04/2026
allan.fuente: [Platform Server/internal/agents/lease.go, Platform Server/internal/agents/watchdog.go, Platform Server/internal/agents/swarm_loop_v3.go, Platform Server/internal/agents/tool_runtime.go, Platform Server/internal/agents/run_store.go, Platform Server/internal/runs/executor.go, Platform Server/internal/runs/durable_store.go, Platform Server/internal/models/openai.go, Platform Server/internal/app/swarm_v3.go, Platform Server/cmd/allan-server/main.go, Platform Server/cmd/allan-server/handlers.go, Platform UI/lib/agent-stream-renderer.ts, docs/arquitectura/intelligent-wait-supervision-design.md, docs/arquitectura/v2-approvals-and-run-manager-design.md]
allan.audiencia: [desarrollador]
allan.madurez: emergente
---

El 18 de julio de 2026 un agente de análisis de transcripciones dejó de
responder a mitad de conversación y la interfaz perdió los iconos de estado. La
autopsia está escrita en `docs/arquitectura/intelligent-wait-supervision-design.md:21-51`
y no describe un fallo, sino tres apilados sobre el mismo defecto. El turno se
ejecutaba en una goroutine cuyo contexto derivaba de `r.Context()`; el
`http.Server` llevaba `WriteTimeout: 5 * time.Minute`, que en Go cubre la
respuesta entera, streaming incluido; a los 300 072 milisegundos exactos el
servidor cortó el socket y la cancelación bajó en cascada. Murió la llamada al
modelo en vuelo, murió el wrap-up y murió la persistencia —`persist root loop
step failed: context canceled`—. El run terminó `failed` con `output_chars=0`:
cinco minutos de trabajo real, cero entregado, estado sin guardar.

Lo interesante del documento no es el diagnóstico sino la frase con la que se
niega a aceptar el arreglo obvio: «La conclusión no es "subir el timeout". Es
que **la plataforma no tiene un modelo de esperas**» (`:53-56`). Ese ensayo
trata de por qué esa negativa es correcta, de la distinción que la sostiene, y
de hasta dónde llega hoy realmente el mecanismo que salió de ahí.

## Un número, dos oficios

Un deadline responde a la vez a dos preguntas que no tienen nada que ver.

La primera es de medición: *¿esto sigue vivo?* El deadline actúa como detector
de fallos. Se elige un umbral, se observa el silencio y se declara muerto lo que
lo excede. La segunda es de política: *¿cuánto recurso concedo a esta
operación?* El deadline actúa como asignador: pasado el umbral, el trabajo se
descarta porque ya no vale lo que cuesta.

Las dos preguntas empujan el mismo número en direcciones opuestas. Como
detector, el umbral quiere ser corto: cuanto antes se note un tool colgado,
antes se replanifica. Como asignador, quiere ser largo: una operación
legítimamente lenta no debería morir por serlo. Con un solo número no se puede
tener las dos cosas, y lo que se obtiene es lo peor de ambas: un umbral
intermedio que tarda demasiado en detectar lo muerto y demasiado poco en tolerar
lo lento. Peor aún, el efecto es el mismo en los dos casos —cancelar—, así que
el sistema no puede ni distinguir después qué le pasó: un tool que tardó dos
minutos y uno que nunca iba a contestar dejan exactamente el mismo rastro.

La dificultad no es de ingeniería descuidada: es un resultado conocido. Chandra
y Toueg lo enuncian como la raíz de las imposibilidades del modelo asíncrono:
«the impossibility results for Consensus and Atomic Broadcast stem from the
inherent difficulty of determining whether a process has actually crashed or is
only "very slow"». Su respuesta no fue construir un detector infalible sino
admitir que se equivoca y caracterizar *cómo*: «each failure detector module can
make mistakes by erroneously adding processes to its list of suspects», y la
clase se define por dos propiedades, «completeness requires that a failure
detector eventually suspects every process that actually crashes, while accuracy
restricts the mistakes that a failure detector can make». Un timeout es un
detector con completeness razonable y accuracy pésima; el error de diseño no es
usarlo, es dejar que ese detector falible ejecute directamente la política.

La literatura de sistemas lleva décadas separando ambas cosas. El detector de
fallos por acumulación de Hayashibara —el que usan Cassandra, Akka y Pekko— hace
exactamente esa separación explícita: «Rather than only answering "yes" or "no"
to the question "is the node down?" it returns a `phi` value representing the
likelihood that the node is down», y la razón declarada es que «An accrual
failure detector decouples monitoring and interpretation». La medición es del
detector; qué hacer con ella es de quien la consume. Dean y Barroso llegan al
mismo sitio desde el lado del rendimiento: «Just as fault-tolerant computing
aims to create a reliable whole out of less reliable parts, we suggest that
large online services need to create a predictably responsive whole out of less
predictable parts». Un componente impredecible en el tiempo no es un componente
roto, y tratarlo como roto es una decisión, no una observación.

Un motor agéntico está en el extremo malo de esta distribución. Una llamada de
modelo con razonamiento puede tardar segundos o minutos según la longitud del
contexto; un tool MCP puede estar paginando doscientos registros de un CRM o
esperando a un servicio que nunca contestará. El propio incidente lo tenía
dentro: `builtin.search/textSearch` tardaba unos dos minutos por llamada
(`intelligent-wait:46-48`) y se comió el reloj de otro. No hay umbral que
distinga eso de un cuelgue.

## Por qué subir el timeout no arregla nada

El descarte está enunciado en el diseño y merece desarrollarse, porque es la
tentación permanente. Subir el timeout desplaza el punto de destrucción; no
cambia lo que pasa al llegar a él. Con 5 minutos, el run del incidente murió a
los 5 minutos; con 30 habría muerto a los 30, después de gastar seis veces más
en modelos. Y el cambio empeora el detector en la misma proporción en que mejora
el asignador: media hora de silencio antes de notar un tool colgado es un
sistema peor, no mejor.

El segundo motivo es que el punto de destrucción no está donde parece. En el
incidente, el `WriteTimeout` mató la *persistencia*, que no era la operación
lenta ni tenía nada que ver con ella; simplemente colgaba del mismo contexto. Un
deadline de misión no acota una operación: acota un subárbol de contextos
completo, y ese subárbol incluye siempre el trabajo de cierre que existe
precisamente para que un fallo no se lleve el estado por delante. De ahí la
regla que quedó (`:148-151`): guardar estado nunca es opcional; ninguna
escritura terminal usa un contexto cancelable por el cliente.

El tercero es que la operación cancelada no es reversible. Cancelar una llamada
de tool y reintentarla desde cero duplica los efectos de todo tool no
idempotente. La cancelación por tiempo, aplicada a un efecto externo, no es una
retirada limpia: es un resultado desconocido.

Otro descarte del corpus va en la dirección contraria y conviene leerlo junto al
anterior. El diseño de aprobaciones rechaza además el timeout global de pared:
«Cortar un run sano por tiempo de pared contradiría ese contrato; el lease
detecta ejecutores muertos sin confundirlos con trabajo lento»
(`v2-approvals-and-run-manager-design.md:222-225`). Es la misma tesis vista
desde la capa de proceso: un lease sirve para saber si el *ejecutor* respira, no
para decidir cuánto puede durar el *trabajo*.

## Cómo lo resuelven los que ya lo resolvieron

Temporal es el sistema que más limpiamente parte el número en dos. Un
*Start-To-Close Timeout* es «the maximum time allowed for a single Activity Task
Execution» —el asignador— y un *Heartbeat Timeout* es «the maximum time between
Activity Heartbeats» —el detector—. El heartbeat es una señal explícita: «An
Activity Heartbeat is a ping from the Worker that is executing the Activity to
the Temporal Service. Each ping informs the Temporal Service that the Activity
Execution is making progress and the Worker has not crashed». Con esa señal, una
actividad legítimamente larga se distingue de un worker caído sin ambigüedad.

Ahora la parte honesta. Temporal separa la medición, pero no separa la política:
al vencer el detector, mata. «If this timeout is reached, the Activity Task fails
and a retry occurs if a Retry Policy dictates it.» No hay evaluación intermedia;
la única gradación posible es la Retry Policy. Y el mecanismo exige cooperación
del código ejecutado: «Activities must heartbeat to receive cancellations from a
Temporal Service». Eso es viable cuando uno escribe las actividades. ALLAN no
escribe los servidores MCP de terceros que invoca: no puede pedirles un
heartbeat, y la única señal de progreso que el protocolo ofrece es opcional. La
lección aplicable de Temporal es la de partir el número; la parte no aplicable es
la de exigir la señal.

LangGraph responde a otra pregunta, y conviene no confundirla con esta. Sus
checkpoints dan durabilidad, no detección: «LangGraph creates a checkpoint at
each super-step boundary. A super-step is a single "tick" of the graph where all
nodes scheduled for that step execute (potentially in parallel)», y el modo de
durabilidad gradúa cuándo se escribe —`"sync"`: «Changes are persisted
synchronously before the next step starts»; `"async"`: «Changes are persisted
asynchronously while the next step executes»; `"exit"`: «Changes are persisted
only when the graph exits»—. Eso hace que un proceso que muere no pierda el
trabajo hecho. No hace nada por un nodo que se cuelga: un nodo colgado sigue
colgado, y ningún checkpoint lo despierta. Es la mitad complementaria del
problema, no la misma mitad. ALLAN adoptó las dos por separado: los checkpoints
de estado están en el ejecutor durable, y la detección está en el lease.

De la literatura de leases viene la tercera pieza. Chubby describe la sesión con
una frase que es exactamente la semántica que hace falta: «Each session has an
associated lease—an interval of time extending into the future during which the
master guarantees not to terminate the session unilaterally», y sobre su
renovación, «The master is free
to advance this timeout further into the future, but may not move it backwards
in time». Un lease es un deadline que solo puede crecer. Y para el problema de
quién actúa cuando un lease caduca en manos de un cliente pausado, Chubby
introduce el *sequencer*: «At any time, a lock holder may request a sequencer, an
opaque byte-string that describes the state of the lock immediately after
acquisition. It contains the name of the lock, the mode in which it was acquired
(exclusive or shared), and the lock generation number». Kleppmann lo reduce a su
esencia —«a fencing token is simply a number that increases (e.g. incremented by
the lock service) every time a client acquires the lock»— y explica el modo de
fallo que motiva todo: «if the GC pause lasts longer than the lease expiry
period, and the client doesn't realise that it has expired, it may go ahead and
make some unsafe change».

## Lo que ALLAN hace hoy

El diseño descartó explícitamente construir un `context.Context` renovable: «no
existe forma limpia de extender un deadline ya fijado en stdlib; usar un
temporizador supervisor separado sobre un contexto normal es más simple y
correcto» (`intelligent-wait:432-435`). El descarte es correcto y la
documentación de Go lo confirma: `WithDeadline` «returns a derived context that
points to the parent context but has the deadline adjusted to be no later than
d», y «If the parent's deadline is already earlier than d, WithDeadline(parent,
d) is semantically equivalent to parent». Los deadlines de contexto solo se
acortan.

Lo que se construyó en su lugar es `runWithLease`
(`internal/agents/lease.go:52`). La función lanza `fn` en una goroutine con su
**propio** contexto derivado del padre (`:56-60`) y arranca un `time.Timer`
fuera. Al vencer el timer no cancela nada: emite un evento `wait` por los stream
callbacks (`:70`, `:231-237`), consulta al watchdog (`:72-78`) y actúa según el
veredicto (`:79-106`). `RENEW` reinicia el timer sobre la **misma** llamada en
vuelo; `NUDGE` inyecta un mensaje correctivo que solo puede aplicarse al paso
siguiente y cae a `RENEW`; `INTERVENE` cancela esa operación y devuelve el error
a la maquinaria de retry existente; `ESCALATE` cancela igual y envuelve el error
como `"escalated: <razón>: %w"`. La cancelación del contexto padre —el run
detenido de verdad— se honra siempre al instante y sin renovar (`:107-110`).

El watchdog (`internal/agents/watchdog.go`) implementa una escalera de coste.
Las dos primeras expiraciones renuevan gratis, sin gastar una llamada a modelo
(`:39`, `:75-78`). A partir de ahí consulta a un modelo `role=fast` con un
prompt que exige JSON estricto (`:90-101`). Ese evaluador corre bajo un timeout
plano de 15 segundos que **no** es un lease (`:34`, `:87`): la recursión «quién
vigila al vigilante» se corta declarando que el vigilante sí acepta un deadline
duro, porque su trabajo sí tiene duración conocida. Y el breaker es real:
cualquier fallo del modelo, timeout o veredicto no parseable devuelve `RENEW`
(`:106-114`). El vigilante nunca mata por accidente.

La asimetría de defaults es el detalle que más dice del diseño. En un lease, la
ambigüedad se resuelve renovando; en un presupuesto agotado, interviniendo
(`:196-234`, y `:202` cuando ni siquiera hay modelo). El motivo está escrito en
el código: renovar un lease es gratis —solo se espera más sobre algo ya en
vuelo—, mientras extender un presupuesto tiene coste real en llamadas a modelo.
El default sigue al coste del error, no a la simetría estética.

Los presupuestos dejaron de ser guillotinas por el mismo mecanismo.
`checkpointBudget` (`swarm_loop_v3.go:784-822`) sustituye al `wrapUp` inmediato:
mide el estado real del run —pasos usados frente a declarados, tool calls,
spawns, hitos cerrados y abiertos, extensiones restantes
(`watchdog.go:129-157`)— y pregunta. La concesión es proporcional y no plana,
con su motivo textual en el propio código: «a flat increment quietly re-imposes
the ceiling the checkpoint exists to remove. A flow declaring max_steps=3 could
never exceed 3+3*4=15 root steps no matter how clearly it was converging»
(`:827-834`). El techo de extensiones es 3 y es independiente del veredicto
(`:759`, `:787`): diga lo que diga el LLM, ningún run se autoextiende sin
límite.

Debajo hay una segunda capa de leases, esta sí en el sentido clásico. El
ejecutor de runs (`internal/runs/executor.go`) desacopla el turno del request con
un `WithoutCancel` reimplementado a mano porque Go 1.20 no lo trae (`:497-508`),
y mantiene el run vivo con `owner_id`, `fence` y un lease renovado por heartbeat
a un tercio del TTL (`:191-236`, TTL 90 s por defecto, `:257`). Las
transiciones son scripts Lua con compare-and-set sobre owner y fence
(`durable_store.go:113-132`, `:169-194`); el estado terminal se persiste con un
contexto propio derivado de `context.Background()` (`executor.go:130`), no del
cliente. Un ejecutor que muere deja su lease vencer y otro proceso lo reconcilia
con un mensaje que dice exactamente lo que pasó: `"run orphaned: executor lease
expired"` (`durable_store.go:278`). Y el fencing token de Kleppmann existe de
verdad en la resolución de aprobaciones: el fence arranca en 1 y se incrementa en
cada reclamación segura (`run_store.go:1122`, `:1136`), mientras un ejecutor que
perdió el lease después de cruzar un límite irreversible no repite la mutación,
sino que deja el estado en `reconciliation_required` (`:1141-1150`).

En la superficie, el `WriteTimeout` global pasó a 0 por defecto
(`cmd/allan-server/main.go:676-690`), el evento `wait` viaja por SSE
(`handlers.go:263-265`) y la interfaz lo pinta como banner en lugar de silencio,
descartándolo en cuanto llega cualquier señal de progreso real
(`Platform UI/lib/agent-stream-renderer.ts:154-161`, `:87`, `:108`).

## Invariantes

Los siguientes enunciados son falsables leyendo el código, y varios tienen test
que los ejercita.

Vencer un lease nunca es, por sí solo, un error: el único camino desde
`timer.C` hasta un retorno con error pasa por un veredicto explícito de
`INTERVENE`/`ESCALATE` o por agotar `maxRenewals` (`lease.go:69-106`). Un fallo
del evaluador nunca produce cancelación: todas las salidas de error de
`Evaluate` devuelven `RENEW` (`watchdog.go:106-114`). El evaluador no puede
colgarse indefinidamente: su contexto es `context.WithTimeout` plano de 15 s
(`:87`), y no existe ningún camino que lo envuelva en un lease. La cancelación
explícita del run se propaga de inmediato: el `case <-ctx.Done()` de
`runWithLease` no consulta al watchdog (`lease.go:107-110`). Un `RENEW` no
reinicia trabajo: `fn` se lanza una sola vez, antes del bucle (`:59-60`).
Ninguna extensión de presupuesto convierte «ilimitado» en «topado»: las subidas
están guardadas por `> 0` (`swarm_loop_v3.go:814-820`). El número de extensiones
está acotado con independencia del veredicto (`:787`). Y ninguna escritura de
estado terminal cuelga de un contexto cancelable por el cliente
(`executor.go:129-137`).

## Lo que está roto o sin terminar

**El lease de las llamadas de modelo está sombreado por un deadline duro del
mismo valor, procedente de la misma variable.** Es el hallazgo más serio de esta
revisión. `StepTimeout` se alimenta de `a.ModelTimeout`
(`internal/app/swarm_v3.go:517`), que es `modelConfig.Timeout`
(`internal/app/app.go:784`), que es `ALLAN_MODEL_TIMEOUT` con default compilado
de 120 s (`internal/models/openai.go:102-108`) y valor desplegado de **30
minutos** (`Platform Server/.env:85` y `Platform Server/config/.env:1`, las dos
capas que el motor lee). Ese mismo valor se impone como deadline
de transporte: `httpClient: &http.Client{Timeout: config.Timeout}` (`:231`) y
`option.WithRequestTimeout(c.config.Timeout)` (`:1653-1658`), y en la ruta de
streaming otro `&http.Client{Timeout: timeout}` (`:1094-1098`). La
documentación de Go es explícita sobre el alcance: «The timeout includes
connection time, any redirects, and reading the response body». En consecuencia,
los cuatro leases de tipo `model_call` (`swarm_loop_v3.go:959`, `:1616`,
`:1773`, `:1872`) no pueden dar más tiempo a la llamada que supervisan: cuando
el timer del lease vence, el transporte está venciendo a la vez, y el error que
llega por `done` es `context.DeadlineExceeded`, que `classifyModelRetry` no
reintenta por decisión explícita (`openai.go:723-729`). El único lease que hoy
puede realmente extender una operación en vuelo es el de `tool_call`, cuyo
`CallTimeout` viene de `ALLAN_TOOL_CALL_TIMEOUT` (default 5 minutos,
`app.go:611-612`) y es independiente de la configuración del proveedor. Dicho de
otro modo: el mecanismo funciona donde se probó (`lease_test.go`, con relojes
sintéticos) y en la ruta de tools, pero en producción la ruta de modelo
conserva, dentro del cliente HTTP, un deadline de misión con el mismo defecto
que el `WriteTimeout` del incidente —incluido el hecho de que cubre la lectura
del cuerpo, es decir, el stream entero—.

**El watchdog no tiene evidencia de vida.** El diseño abría con una tabla de
señales de liveness por tipo de espera —tokens del stream, pasos completados de
worker, notificaciones de progreso MCP (`:168-179`)—, y D4 la recortó
deliberadamente: «no hay liveness-tracking basado en tokens (`OnToken`)
alimentando al watchdog directamente» (`:498-511`). Hoy `Evaluate` recibe el
tipo, el sujeto, el número de renovaciones y un tail de transcripción de cuatro
mensajes (`lease.go:129-145`); nada más. La consecuencia práctica es que un
stream que lleva veinte minutos emitiendo tokens es, para el supervisor,
indistinguible de un socket mudo. La ironía es que esa señal sí existe y sí se
usa: la interfaz limpia el banner de espera en cuanto llega un token
(`agent-stream-renderer.ts:87`). La capa de presentación sabe más sobre la
vitalidad de la operación que el componente encargado de juzgarla.

**`ESCALATE` no escala.** El propio D4 lo declara: «`ESCALATE` tampoco integra
todavía con el protocolo `waiting_human` completo… por ahora produce un run
fallido con mensaje claro, no una pausa real» (`:507-511`). Verificado: la
cadena `"escalated:"` solo aparece en `lease.go:89` y no tiene ningún consumidor
en el árbol. Es una etiqueta en un mensaje de error, no una transición de
estado.

**`INTERVENE` sobre un tool reintroduce el problema de la duplicación.** El
bucle de reintentos vuelve a llamar con los mismos argumentos
(`tool_runtime.go:327-330`) y `CallTool` recibe solo `(tool, arguments)`
(`:497`): no hay clave de idempotencia propagada al conector ni marca de
«resultado desconocido». La mitigación que el propio diseño describe en su
sección de riesgos (`:295-299`) no está implementada. El lease reduce la
frecuencia con que esto ocurre —al no cancelar en el primer vencimiento— pero no
cierra el agujero.

**El `fence` de los runs HTTP no es un fencing token.** El script de creación lo
fija literalmente en `"1"` (`durable_store.go:68`) y no existe ningún punto del
árbol que lo incremente; `create` además rechaza la clave si ya existe (`:59-61`),
de modo que no hay toma de relevo que fencear. El discriminador real es
`owner_id`. Sin takeover no hay incorrección, pero el nombre promete una
propiedad —monotonía frente a un dueño rezagado, en el sentido de Kleppmann— que
en esta ruta no está. Donde sí está es en las aprobaciones
(`run_store.go:1136`), y el contraste entre ambas rutas no está documentado en
ningún diseño.

**La configuración del §9 del diseño no existe.** `ALLAN_LEASE_MODEL_STREAM`,
`ALLAN_LEASE_MODEL_BLOCKING`, `ALLAN_LEASE_TOOL_CALL`,
`ALLAN_WATCHDOG_MAX_RENEWALS`, `ALLAN_WATCHDOG_ROLE` y `ALLAN_RESUME_ON_BOOT`
no aparecen en el árbol. Las únicas variables reales son `ALLAN_WRITE_TIMEOUT`
(`main.go:677`) y `ALLAN_RUN_LEASE_TTL` (`runs/executor.go:258`), que no figura
en la tabla. `MaxLeaseRenewals` solo se fija explícitamente en un sitio —a 3,
para la reanudación de aprobaciones (`internal/app/approvals.go:338`)—; la ruta
de producción (`internal/app/swarm_v3.go:471-540`) no lo fija, así que cae al
default 10 (`lease.go:16`).

Conviene ver esa aritmética junta, porque nadie la escribió. Con los defaults,
una llamada de tool expira a los 5 minutos, se renueva gratis dos veces y solo a
los **15 minutos** se consulta por primera vez al evaluador; si este insiste en
`RENEW`, el corte duro llega a los 55 minutos, y con `MaxRetries=2`
(`swarm_loop_v3.go:441`) hay tres intentos. Son magnitudes defendibles para un
motor que aspira a trabajar durante horas, pero no son las que sugiere leer «2
renovaciones gratis» en el código.

**La traza por veredicto de lease no existe; el coste sí se atribuye.** El
checkpoint de presupuesto escribe un `swarm_v3_budget_extended` en el trace
(`swarm_loop_v3.go:799-807`), pero los veredictos de lease solo salen por
`debugf` (`watchdog.go:115`). En cambio la afirmación de que el watchdog no se
mide es incorrecta: todas sus llamadas van etiquetadas como consumidor
`engine.watchdog` (`watchdog.go:104`, `:221`; `models/context.go:104`). Lo que
falta es el rastro auditable de la decisión, no la factura.

**El bucle de worker sigue sin checkpoint.** D5 se acotó al root de forma
explícita (`:560-569`); un worker que agota sus pasos termina en `partial`. La
promesa «el presupuesto es un punto de control, no una guillotina» vale hoy para
el orquestador y no para sus subordinados.

## Qué queda de todo esto

La tesis, despojada del incidente que la produjo, es que un deadline que mata es
una decisión de política disfrazada de medición. Separar las dos cosas cuesta
poco: un temporizador que despierta a alguien en vez de cancelar un contexto, y
un evaluador escalonado que empieza gratis. Lo que se gana es que el sistema
puede por fin decir cosas distintas sobre situaciones distintas —«sigue vivo»,
«va perdido», «esta operación está muerta», «esto lo tiene que ver una
persona»— en lugar de traducirlas todas a `context canceled`.

Lo que ALLAN construyó es esa separación, y funciona en la capa de proceso
—leases de ejecutor con CAS, reconciliación honesta de huérfanos, fence real en
aprobaciones— y en la ruta de tools. Lo que no ha terminado de hacer es
propagarla hacia abajo: mientras el cliente de modelos siga imponiendo un
deadline duro con el mismo valor que el lease que lo supervisa, la ruta más
frecuente del motor sigue teniendo un deadline de misión, solo que ahora
escondido dos capas más abajo y con un supervisor encima que cree estar
renovándolo.

## Referencias

**Burrows, M. (2006).** *The Chubby lock service for loosely-coupled distributed
systems*. OSDI '06.
https://static.googleusercontent.com/media/research.google.com/en//archive/chubby-osdi06.pdf
— «Each session has an associated lease—an interval of time extending into the
future during which the master guarantees not to terminate the session
unilaterally.» *(Corrección de esta revisión: una versión anterior omitía
«extending into the future» sin marcar la elisión; la frase se restituye
completa contra el PDF de OSDI '06, §2.)* · «The master is
free to advance this timeout further into the future, but may not move it
backwards in time.» · «At any time, a lock holder may request a *sequencer*, an
opaque byte-string that describes the state of the lock immediately after
acquisition. It contains the name of the lock, the mode in which it was acquired
(exclusive or shared), and the lock generation number.» · «Thus, a process
holding a lock L may issue a request R, but then fail. Another process may
acquire L and perform some action before R arrives at its destination.»

**Chandra, T. D., Toueg, S. (1996).** *Unreliable Failure Detectors for Reliable
Distributed Systems*. Journal of the ACM 43(2).
https://www.cs.princeton.edu/courses/archive/fall08/cos597B/papers/unreliable.pdf
— «Essentially, the impossibility results for Consensus and Atomic Broadcast
stem from the inherent difficulty of determining whether a process has actually
crashed or is only "very slow".» · «We assume that each failure detector module
can make mistakes by erroneously adding processes to its list of suspects: i.e,
it can suspect that a process p has crashed even though p is still running.» ·
«Roughly speaking, *completeness* requires that a failure detector eventually
suspects every process that actually crashes, while *accuracy* restricts the
mistakes that a failure detector can make.»

**Dean, J., Barroso, L. A. (2013).** *The Tail at Scale*. Communications of the
ACM 56(2). https://research.google/pubs/the-tail-at-scale/ — «It is challenging
to keep the tail of the latency distribution low for interactive services as the
size and complexity of the system scales up or as overall utilization
increases.» · «Just as fault-tolerant computing aims to create a reliable whole
out of less reliable parts, we suggest that large online services need to create
a predictably responsive whole out of less predictable parts.»

**Go Authors.** *Package context*. https://pkg.go.dev/context — «WithDeadline
returns a derived context that points to the parent context but has the deadline
adjusted to be no later than d.» · «If the parent's deadline is already earlier
than d, WithDeadline(parent, d) is semantically equivalent to parent.» ·
«WithoutCancel returns a derived context that points to the parent context and
is not canceled when parent is canceled.»

**Go Authors.** *Package net/http*, campo `Client.Timeout`.
https://pkg.go.dev/net/http — «Timeout specifies a time limit for requests made
by this Client. The timeout includes connection time, any redirects, and reading
the response body. The timer remains running after Get, Head, Post, or Do return
and will interrupt reading of the Response.Body.»

**Gray, C. G., Cheriton, D. R. (1989).** *Leases: An Efficient Fault-Tolerant
Mechanism for Distributed File Cache Consistency*. SOSP '89.
https://dl.acm.org/doi/10.1145/74851.74870 — **NO VERIFICADA.** Se intentó
recuperar el texto por tres vías (dos réplicas universitarias en PDF, que
fallaron en la verificación del certificado TLS; y el registro de la ACM, de
acceso restringido). No se ha podido extraer ninguna frase literal, de modo que
ninguna afirmación del cuerpo se apoya en esta fuente; se lista únicamente como
origen convencionalmente atribuido del término *lease*, atribución que esta
revisión **no** ha comprobado.

**Hayashibara, N. et al. / Apache Pekko.** *Phi Accrual Failure Detector*
(documentación de Pekko, que implementa el detector del artículo homónimo de
2004). https://pekko.apache.org/docs/pekko/1.3/typed/failure-detector.html —
«Rather than only answering "yes" or "no" to the question "is the node down?" it
returns a `phi` value representing the likelihood that the node is down.» · «An
accrual failure detector decouples monitoring and interpretation. That makes
them applicable to a wider area of scenarios and more adequate to build generic
failure detection services.»

**Kleppmann, M. (2016).** *How to do distributed locking*.
https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html — «In
this context, a fencing token is simply a number that increases (e.g.
incremented by the lock service) every time a client acquires the lock.» · «The
lock has a timeout (i.e. it is a lease), which is always a good idea (otherwise a
crashed client could end up holding a lock forever and never releasing it).» ·
«However, if the GC pause lasts longer than the lease expiry period, and the
client doesn't realise that it has expired, it may go ahead and make some unsafe
change.» · «Redlock assumes that delays, pauses and drift are all small relative
to the time-to-live of a lock; if the timing issues become as large as the
time-to-live, the algorithm fails.»

**LangChain (2026).** *Checkpointers* (documentación de LangGraph).
https://docs.langchain.com/oss/python/langgraph/checkpointers — «LangGraph
creates a checkpoint at each super-step boundary. A super-step is a single
"tick" of the graph where all nodes scheduled for that step execute (potentially
in parallel).» · «Checkpoints are persisted and can be used to restore the state
of a thread at a later time.»

**LangChain (2026).** *Durability* (referencia de la API de LangGraph).
https://reference.langchain.com/python/langgraph/types/Durability — «Changes are
persisted synchronously before the next step starts.» (`"sync"`) · «Changes are
persisted asynchronously while the next step executes.» (`"async"`) · «Changes
are persisted only when the graph exits.» (`"exit"`)

**Temporal (2026).** *Detecting Activity Failures: Key Timeouts and Heartbeats*.
https://docs.temporal.io/encyclopedia/detecting-activity-failures — «An Activity
Heartbeat is a ping from the Worker that is executing the Activity to the
Temporal Service. Each ping informs the Temporal Service that the Activity
Execution is making progress and the Worker has not crashed.» · «A Heartbeat
Timeout is the maximum time between Activity Heartbeats.» · «A Start-To-Close
Timeout is the maximum time allowed for a single Activity Task Execution.» · «If
this timeout is reached, the Activity Task fails and a retry occurs if a Retry
Policy dictates it.»

**Temporal (2026).** *Activity Execution*. https://docs.temporal.io/activity-execution
— «Activities must heartbeat to receive cancellations from a Temporal Service.»
