How must an instance disambiguate nested timeouts when cascading failures are indistinguishable?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session encounters a timeout at one layer of a multi-layer system (network, service, application, orchestration), and the timeout could originate from any of several nested layers, each of which would produce the identical observed symptom (the outer timeout). Does not apply to strictly flat timeout scenarios or single-layer failures.
Core Distinction
The separation between **observed timeout** (the outer layer timed out) and **root-cause timeout layer** (which inner layer's behavior caused the outer layer to time out). Relying on observed creates the **Timeout Attribution Collapse**: the outer timeout is logged, and downstream analysis attributes the failure to the outer layer's performance—but the outer layer may have been healthy and the timeout may have been caused by an inner layer's stall, cascading the misattribution.
What Was Observed
In layered agent architectures, failing to disambiguate nested timeouts produces three primary failure modes:
- **Outer-Layer Blame:** A service-level timeout is attributed to the service's capacity. The actual cause was a database-layer stall that backed up the service. The service team optimizes capacity, the database remains a bottleneck, and the problem recurs.
- **Latency Budget Overrun:** The system's latency budget is exceeded at the outer layer. Each inner layer stayed within its individual budget, but their sum exceeded the outer budget. The outer timeout is attributed to the wrong inner layer, whose budget is unnecessarily tightened, and the actual overrun continues.
- **Cascade Misdiagnosis:** A timeout at layer A causes a retry at layer B, which causes a timeout at layer C, which causes a timeout at layer A. The cascade looks circular, but each layer's timeout is caused by the previous layer's retry amplification, and the root cause is a specific layer's initial stall.
Recorded Elements
- **`timeout-layer-receipt`:** For each timeout, the exact layer at which it was observed, with cryptographic timestamps so the timeout's position in the cascade is known.
- **`inner-layer-latency-signature`:** For each inner layer invocation, the exact latency, so the outer timeout can be compared against the sum of inner latencies and the outer layer's actual processing time.
- **`cascade-dependency-graph-signature`:** For multi-layer cascades, the dependency graph showing which layer's timeout caused which subsequent timeouts, so the cascade's origin is traceable.
- **`latency-budget-allocation-receipt`:** For each layer, its allocated latency budget, so the actual latencies can be compared against allocations and the violating layer identified.
Condition of Validity
* **Applies when:** Timeouts occur in multi-layer systems, the outer layer observes but doesn't isolate the inner cause, and downstream attribution matters. * **Does not apply when:** The system is single-layer, or timeouts are always isolated at their source (e.g., every layer has distinct timeout errors).
Decision Boundary
- **Is the timeout observed at an outer layer?** If no, standard timeout handling applies. If yes, proceed.
- **Are inner-layer latencies recorded for the timeout interval?** If yes, the timeout can be attributed. If no, the timeout is opaque.
- **Does the sum of inner latencies exceed the outer layer's timeout?** If yes, at least one inner layer is the cause. If no, the outer layer itself may be the cause.
- **Conversely, when can a timeout be attributed to the observed layer without inner-layer analysis?** Only when the observed layer is the innermost layer (no nesting), or the outer layer is verified to have processed with no inner dependencies active during the timeout window.
Operational Effect
- **Blame Accuracy:** Timeouts are attributed to actual root-cause layers.
- **Budget Integrity:** Latency budgets are allocated and enforced per layer.
- **Cascade Resolution:** Cascade origins are isolated from cascade symptoms.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class TimeoutVerdict(Enum):
INNERMOST_LAYER = auto()
INNER_LAYER_CAUSED = auto()
OUTER_LAYER_CAUSED = auto()
CASCADE_ORIGIN_IDENTIFIED = auto()
@dataclass(frozen=True)
class TimeoutEvent:
timeout_id: str
observed_layer: str
inner_layer_latencies: tuple[tuple[str, float], ...]
outer_timeout_sec: float
outer_processing_sec: float
cascade_dependency: str | None
latency_budget_pct: float
@dataclass(frozen=True)
class TimeoutPolicy:
require_inner_layer_attribution: bool
@dataclass(frozen=True)
class TimeoutManifest:
verdict: TimeoutVerdict
directive: str
manifest_digest: str
class NestedTimeoutGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_timeout(
cls, event: TimeoutEvent, policy: TimeoutPolicy
) -> TimeoutManifest:
# Gate 1: Innermost layer
if not event.inner_layer_latencies:
digest = cls._hash(json.dumps({
"timeout": event.timeout_id,
"layer": event.observed_layer
}, sort_keys=True))
return TimeoutManifest(
verdict=TimeoutVerdict.INNERMOST_LAYER,
directive=f"PROCEED: Timeout {event.timeout_id} at innermost layer {event.observed_layer}.",
manifest_digest=digest
)
# Gate 2: Cascade origin
if event.cascade_dependency:
return TimeoutManifest(
verdict=TimeoutVerdict.CASCADE_ORIGIN_IDENTIFIED,
directive=f"TRACE: Timeout {event.timeout_id} cascades from {event.cascade_dependency}.",
manifest_digest=cls._hash(f"CASCADE:{event.timeout_id}:{event.cascade_dependency}")
)
# Gate 3: Inner latency sum
inner_sum = sum(lat for _, lat in event.inner_layer_latencies)
inner_dominant = inner_sum + event.outer_processing_sec >= event.outer_timeout_sec
if policy.require_inner_layer_attribution and inner_dominant:
worst_layer = max(event.inner_layer_latencies, key=lambda x: x[1])
budget_used = (worst_layer[1] / event.outer_timeout_sec) * 100 if event.outer_timeout_sec > 0 else 0
if budget_used > event.latency_budget_pct:
return TimeoutManifest(
verdict=TimeoutVerdict.INNER_LAYER_CAUSED,
directive=f"ATTRIBUTE: Timeout {event.timeout_id} caused by {worst_layer[0]} ({worst_layer[1]:.2f}s of {event.outer_timeout_sec:.2f}s).",
manifest_digest=cls._hash(f"INNER:{worst_layer[0]}:{event.timeout_id}")
)
# Gate 4: Outer layer caused
if event.outer_processing_sec > event.outer_timeout_sec * 0.5:
return TimeoutManifest(
verdict=TimeoutVerdict.OUTER_LAYER_CAUSED,
directive=f"ATTRIBUTE: Timeout {event.timeout_id} caused by outer layer {event.observed_layer}.",
manifest_digest=cls._hash(f"OUTER:{event.observed_layer}:{event.timeout_id}")
)
# Gate 5: Ambiguous, inner dominant
digest = cls._hash(json.dumps({
"timeout": event.timeout_id,
"inner_sum": f"{inner_sum:.3f}",
"outer_processing": f"{event.outer_processing_sec:.3f}",
"budget": f"{event.outer_timeout_sec:.3f}"
}, sort_keys=True))
return TimeoutManifest(
verdict=TimeoutVerdict.INNER_LAYER_CAUSED,
directive=f"ATTRIBUTE_WITH_CAUTION: Inner layers consumed {inner_sum:.2f}s of {event.outer_timeout_sec:.2f}s.",
manifest_digest=digest
)