---
name: chat-recovery
title: "Chat Recovery"
description: "Engine substrate for the chat tool-loop circuit breaker and round-exhaustion handler — the strategy layer for 'what do we say when the turn can't complete?'"
category: _meta
status: active
priority: 9
---

## Instructions

This is a `_meta` skill — an engine-level substrate, not a behavioral skill. It is not injected into any agent's system prompt. Its purpose is to own the STRATEGY for two related failure modes in the chat tool loop:

1. **Error fatigue (Fix 5)**: the LLM keeps calling tools that keep returning errors. After N rounds where the error rate exceeds a YAML-tuned threshold, the engine breaks out of the loop and tells the user what happened instead of trusting the LLM to detect its own failure state.
2. **Round exhaustion (Fix 1)**: the LLM calls tools for every round up to `max_rounds` without ever emitting "no tool calls". Previously (pre-Fix) cloud mode abandoned all accumulated tool context and re-streamed from raw history — producing a generic preamble that promised work that had already been attempted. The unified handler now summarises what was tried using the full `messages` context.

Both failure modes feed into the same recovery path: a single LLM call that receives the full tool-call trail and is told to report honestly.

### The recovery prompt

When the engine triggers recovery, it calls the LLM with:
- The conversation history (so the user's original question is visible)
- The accumulated `messages` list (every tool call + result across rounds)
- A directive string assembled from `config/recovery.yaml`:`directive`

The directive tells the LLM to:
- Summarise ONLY the tool calls that actually ran and what they returned
- Never invent or narrate beyond the actual results
- Be explicit about failures ("I tried to research X but 5/5 scrapes failed with timeouts")
- End with a concrete next step (retry, ask the user for clarification, escalate)

### The deterministic fallback

If the recovery LLM call ALSO fails (times out, returns junk, hits another exception), the engine falls back to a `config/recovery.yaml`:`fallback_template` string. It's a dumb templated message ("I attempted {tools_attempted} tool(s) during this turn. {success_count} succeeded, {error_count} failed. The stream ended before I could summarise. Please retry or ask me in a different way.") — not pretty, but it's honest and it's ALWAYS produced.

## Rules

- Never let the LLM "decide" whether to report failure. That's the bug we're closing — GPT-5.4 and other cloud models narrate success even when all tools failed.
- The YAML thresholds (`min_rounds_before_break`, `error_rate_threshold`) are strategy and live in `config/recovery.yaml`. Do NOT hardcode them in Python.
- Every recovery trigger emits a high-importance activity event (`llm/chat_circuit_broken` or `llm/chat_round_exhausted`) so the user can see it happened AND SkillSchool can analyze the pattern.
- The recovery handler MUST persist an assistant message. If both the LLM call and the fallback template fail, the Fix 7 finally-block sentinel catches it.

## Tone

Config-driven. This file is documentation for maintainers.

## Examples

**Circuit breaker fires**: 4 consecutive tool calls return `{"error": ...}`. Engine inspects the last 2 rounds, sees error_rate = 4/4 = 1.0 ≥ threshold 0.5, triggers recovery. LLM summarises: "I tried to scrape 4 URLs but all returned errors (3 timeouts, 1 403 forbidden). I'd suggest checking the source availability or reducing the count."

**Round exhaustion fires**: LLM calls tools for all 5 rounds without ever settling. Engine triggers recovery with the full messages list. LLM: "Across 5 rounds I called create_kb (succeeded), run_wiki_job (0 articles — KB was empty), web_search (2 results), and research_scrape (1 of 3 succeeded). The scrapes that failed were X and Y. Would you like me to retry those with a different source?"

**Both paths fail**: LLM recovery call times out. Engine writes the deterministic fallback: "I attempted 9 tool(s) during this turn. 3 succeeded, 6 failed. The stream ended before I could summarise. Please retry or ask me in a different way."
