# How Stagnation Detection in Ouroboros Identifies Spinning, Oscillation, and No-Drift Patterns

> Learn how Ouroboros Stagnation Detection analyzes SHA-256 hashes to identify spinning, oscillation, and no-drift execution patterns using a stateless detector and execution history.

- Repository: [Q00/ouroboros](https://github.com/Q00/ouroboros)
- Tags: deep-dive
- Published: 2026-03-14

---

**Stagnation Detection in Ouroboros identifies non-productive execution loops by analyzing SHA‑256 hashed outputs for repetition, alternating state sequences for oscillation, and minimal drift-score deltas for stagnation, using a stateless detector that processes an `ExecutionHistory` object.**

The Ouroboros framework employs a specialized `StagnationDetector` class to identify when AI agent execution enters non-productive loops. Implemented in [`src/ouroboros/resilience/stagnation.py`](https://github.com/Q00/ouroboros/blob/main/src/ouroboros/resilience/stagnation.py), this stateless component analyzes recent execution history to flag three distinct failure patterns: spinning, oscillation, and no-drift conditions.

## How the StagnationDetector Works

The `StagnationDetector` class operates as a pure function that accepts an **`ExecutionHistory`** object containing recent phase outputs, error signatures, and drift scores. It computes SHA‑256 hashes of discrete outputs to enable efficient pattern matching while avoiding storage of large text fragments. The detector invokes three specialized internal methods—`_detect_spinning`, `_detect_oscillation`, and `_detect_no_drift`—each returning a `StagnationDetection` object when their respective threshold conditions are met.

## Pattern Detection Algorithms

### Detecting Spinning Patterns

Spinning occurs when the same output or error repeats consecutively without progress. The `_detect_spinning` method (lines 247‑274) implements this check by:

1. Retrieving the last *N* outputs from `history.phase_outputs`, where *N* equals the `spinning_threshold` (default **3**).
2. Computing a short SHA‑256 hash for each entry using `_compute_hash`.
3. Returning a `StagnationDetection` with `pattern = SPINNING` if all hashes match.
4. If output hashing fails, repeating the process against `history.error_signatures`.

When detected, the evidence includes the `repeated_output_sample`, `repeat_count`, and `source` field indicating whether the repetition originated from outputs or errors.

### Detecting Oscillation Patterns

Oscillation represents an alternating A→B→A→B state flip that prevents convergence. The `_detect_oscillation` method (lines 300‑345) identifies this by:

1. Validating that at least `oscillation_cycles` × 2 items exist in recent outputs (default **2 cycles**, requiring **4 items**).
2. Splitting the hashed outputs into **even-indexed** (0, 2, …) and **odd-indexed** (1, 3, …) groups.
3. Confirming uniform hashes within each group and inequality between groups.
4. Returning `pattern = OSCILLATION` when the alternating condition holds.

This detection catches flip-flopping behaviors where the system alternates between two distinct states without making forward progress.

### Detecting No-Drift Patterns

No-drift stagnation occurs when semantic drift scores remain nearly constant, indicating the agent is not evolving its understanding. The `_detect_no_drift` method (lines 352‑383) monitors `history.drift_scores` by:

1. Examining the last *N* scores, where *N* equals `no_drift_iterations` (default **3**).
2. Computing absolute deltas between successive scores.
3. Returning `pattern = NO_DRIFT` if **every** delta falls below `no_drift_epsilon` (default **0.01**).

Confidence scoring derives from proximity to the epsilon threshold using the formula `1 - average_delta/epsilon`, providing a normalized certainty metric for downstream recovery decisions.

## Implementation Examples

### Example 1 – Detecting Spinning

```python
from ouroboros.resilience.stagnation import StagnationDetector, ExecutionHistory

history = ExecutionHistory.from_lists(
    phase_outputs=["error A", "error A", "error A"],   # three identical outputs

    error_signatures=[],
    drift_scores=[0.5, 0.5],
    iteration=3,
)

detector = StagnationDetector()
result = detector.detect(history)

for detection in result.value:
    if detection.pattern.name == "SPINNING" and detection.detected:
        print("Spinning detected!", detection.evidence)

```

Output:

```

Spinning detected! {'repeated_output_sample': 'error A', 'repeat_count': 3, 'source': 'phase_outputs'}

```

### Example 2 – Detecting Oscillation

```python
history = ExecutionHistory.from_lists(
    phase_outputs=["state A", "state B", "state A", "state B"],
    error_signatures=[],
    drift_scores=[0.45, 0.44, 0.45, 0.44],
    iteration=4,
)

detector = StagnationDetector(oscillation_cycles=2)   # default is 2 cycles

result = detector.detect(history)

osc = next(d for d in result.value if d.pattern == StagnationPattern.OSCILLATION)
if osc.detected:
    print("Oscillation found:", osc.evidence)

```

### Example 3 – Detecting No-Drift

```python
history = ExecutionHistory.from_lists(
    phase_outputs=["out1", "out2"],
    error_signatures=[],
    drift_scores=[0.30, 0.301, 0.302],   # changes < 0.01

    iteration=3,
)

detector = StagnationDetector(no_drift_epsilon=0.01, no_drift_iterations=3)
result = detector.detect(history)

no_drift = next(d for d in result.value if d.pattern == StagnationPattern.NO_DRIFT)
if no_drift.detected:
    print("No drift:", no_drift.evidence)

```

## Summary

- **Stagnation Detection in Ouroboros** uses a stateless `StagnationDetector` class that analyzes `ExecutionHistory` objects to identify three non-productive execution patterns.
- **Spinning detection** relies on SHA‑256 hashing to identify identical outputs or errors repeating for at least three consecutive iterations (default threshold).
- **Oscillation detection** splits hashed outputs into even and odd indices to verify A→B→A→B alternating patterns over four or more items (default 2 cycles).
- **No-drift detection** monitors semantic drift-score deltas, flagging stagnation when consecutive changes remain below 0.01 for three or more iterations.
- All patterns return structured `StagnationDetection` objects containing boolean flags, evidence dictionaries, and confidence scores suitable for automated recovery workflows.

## Frequently Asked Questions

### What is the default spinning threshold in Ouroboros stagnation detection?

The default `spinning_threshold` is **3**, meaning the detector flags a spinning pattern when the same output or error signature repeats for three consecutive iterations. This value is configurable via the `StagnationDetector` constructor.

### How does the oscillation detector distinguish between normal variation and harmful oscillation?

The detector specifically looks for alternating state patterns by comparing SHA‑256 hashes of even-indexed versus odd-indexed outputs in the recent history. It only flags `OSCILLATION` when the even group shares one identical hash, the odd group shares a different identical hash, and the pattern spans at least two full cycles (four items by default).

### Can the no-drift detection sensitivity be adjusted for different agent behaviors?

Yes, the sensitivity is controlled by two parameters: `no_drift_epsilon` (default 0.01) sets the maximum allowed delta between consecutive drift scores, and `no_drift_iterations` (default 3) sets the required number of consecutive low-delta measurements. Increasing the epsilon makes the detector less sensitive to small fluctuations.

### Where is the stagnation detection logic implemented in the Ouroboros codebase?

The core logic resides in **[`src/ouroboros/resilience/stagnation.py`](https://github.com/Q00/ouroboros/blob/main/src/ouroboros/resilience/stagnation.py)**, which contains the `StagnationDetector` class and the three private detection methods: `_detect_spinning` (lines 247‑274), `_detect_oscillation` (lines 300‑345), and `_detect_no_drift` (lines 352‑383).