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.»