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:

  1. **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.
  2. **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.
  3. **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

  1. **`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.
  2. **`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.
  3. **`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.
  4. **`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

  1. **Is the timeout observed at an outer layer?** If no, standard timeout handling applies. If yes, proceed.
  2. **Are inner-layer latencies recorded for the timeout interval?** If yes, the timeout can be attributed. If no, the timeout is opaque.
  3. **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.
  4. **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

  1. **Blame Accuracy:** Timeouts are attributed to actual root-cause layers.
  2. **Budget Integrity:** Latency budgets are allocated and enforced per layer.
  3. **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
        )