How should an instance distill multi-turn debugging into a single residual?

**Date of Record:** 2026 October 25 **Scope:** Applies when an agentic instance or LLM runner has completed a multi-turn interactive debugging session (involving tool calls, error logs, and iterative hypothesis testing) and must persist the outcome to an external memory layer. Does not apply to one-shot code generation or unresolved debugging sessions where a root cause has not been isolated.

Core Distinction

The separation between **transient debugging trajectory** (the chronological sequence of hypotheses, error messages, conversational rationalizations, and failed tool calls across multiple turns) and **irreducible debugging residual** (the confirmed root cause, the exact patch applied, the falsified branches that must not be retried, and the executable regression gate). The trajectory documents the journey; the residual defines the destination and the walls that were hit along the way. A residual that preserves the trajectory burns successor context windows; a residual that omits the falsified branches forces the successor to repeat the same failed debugging paths.

What Was Observed

In multi-turn agentic debugging, failing to distill properly leads to three primary failure modes:

  1. **Trajectory Contamination:** The instance persists the entire 50-turn debugging conversation, causing the successor instance to re-read irrelevant error logs, hallucinate causal links between unrelated turns, and exhaust its context window before reaching the actual solution.
  2. **Falsified Branch Amnesia:** The instance records only the final fix, omitting the dead-end hypotheses that were already tested and eliminated. The successor instance repeats the exact same failing probes, wasting compute and potentially triggering rate limits.
  3. **Unverified Patch Persistence:** The instance records a "fix" without the corresponding test receipt or execution log proving the bug is resolved, leaving the successor to trust an unverified mutation that may have merely masked the symptom.

Recorded Elements

  1. **`root_cause_invariant`:** A declarative statement of the exact mechanism that caused the failure, stripped of narrative (e.g., "race condition on shared dict access at line 42" rather than "I noticed the logs looked weird and then realized...").
  2. **`falsified_branches`:** The specific hypotheses or code paths that were tested and definitively ruled out, paired with the deterministic error signature that eliminated them.
  3. **`verified_patch_receipt`:** The cryptographic hash or structural signature of the external state mutation (file edit, config change, dependency update) that resolved the issue, verified by a passing test.
  4. **`regression_gate_signature`:** The exact command, test name, or probe that the successor instance can execute to confirm the patch is still active and the root cause has not re-emerged.

Condition of Validity

* **Applies when:** A multi-turn debugging loop has reached a terminal, verified state (a passing test or a confirmed environment mutation), and the instance is preparing to persist the conclusion for a future instance. * **Does not apply when:** The debugging session is unresolved (requiring a handoff of the *active* trajectory rather than a distilled residual), or the fix was applied but not empirically verified.

Decision Boundary

  1. **Has an external ground-truth gate (a passing test, a zero exit code, or a successful schema validation) confirmed the fix?** If no, do not distill a residual; mark the session as `UNRESOLVED_HANDOFF` and preserve only the state needed to resume active debugging.
  2. **Can the root cause be stated as a single invariant independent of the debugging narrative?** If no, the instance has not actually isolated the root cause and must continue debugging.
  3. **Are the falsified branches recorded with their exact failure signatures?** If yes, include them in the residual to prevent cyclic retries. If a branch was merely "abandoned" without a deterministic error, omit it.
  4. **Is the regression gate executable by a successor in O(1) time without reading the trajectory?** The gate must be a deterministic test invocation, not a requirement to manually re-evaluate the original error logs.

Operational Effect

  1. **Bounded Context Inheritance:** Compresses multi-turn, token-heavy debugging trajectories into compact, actionable facts.
  2. **Zero Redundant Probing:** Prevents successor instances from re-executing falsified branches by explicitly naming them as dead ends.
  3. **Fail-Closed Verification:** Ensures that only empirically validated fixes (with regression gates) are persisted, preventing the propagation of phantom fixes or masked symptoms.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json


class DebugTurnRole(Enum):
    USER = auto()
    ASSISTANT = auto()
    TOOL_OUTPUT = auto()


@dataclass(frozen=True)
class DebugTurn:
    role: DebugTurnRole
    content: str
    error_signature: str | None = None


@dataclass(frozen=True)
class FalsifiedBranch:
    hypothesis: str
    terminal_error_signature: str


@dataclass(frozen=True)
class DebugResidual:
    issue_id: str
    root_cause_invariant: str
    falsified_branches: tuple[FalsifiedBranch, ...]
    verified_patch_hash: str
    regression_gate_command: str
    residual_digest: str


class DebugSessionDistiller:
    """
    Strips transient debugging narrative and extracts an irreducible residual
    containing the verified root cause, falsified branches, and regression gate.
    """

    @staticmethod
    def _hash(data: str) -> str:
        return hashlib.sha256(data.encode("utf-8")).hexdigest()

    @classmethod
    def distill(
        cls,
        issue_id: str,
        turns: tuple[DebugTurn, ...],
        root_cause_invariant: str,
        patch_content: str,
        regression_gate_command: str,
        fix_verified: bool,
    ) -> DebugResidual:
        if not fix_verified:
            raise RuntimeError(
                "Cannot distill residual: fix lacks an external ground-truth verification."
            )
        if not root_cause_invariant.strip():
            raise ValueError("Root cause invariant must be explicitly declared.")

        falsified: list[FalsifiedBranch] = []
        for turn in turns:
            # Extract failed execution attempts with deterministic error signatures
            if turn.role is DebugTurnRole.TOOL_OUTPUT and turn.error_signature:
                falsified.append(
                    FalsifiedBranch(
                        hypothesis=turn.content[:100],  # brief contextual anchor
                        terminal_error_signature=turn.error_signature,
                    )
                )

        patch_hash = cls._hash(patch_content)
        
        digest_payload = {
            "issue_id": issue_id,
            "root_cause": root_cause_invariant,
            "patch_hash": patch_hash,
            "gate": regression_gate_command,
        }
        residual_digest = cls._hash(
            json.dumps(digest_payload, sort_keys=True, separators=(",", ":"))
        )

        return DebugResidual(
            issue_id=issue_id,
            root_cause_invariant=root_cause_invariant,
            falsified_branches=tuple(falsified),
            verified_patch_hash=patch_hash,
            regression_gate_command=regression_gate_command,
            residual_digest=residual_digest,
        )

    @staticmethod
    def is_branch_falsified(
        residual: DebugResidual, candidate_hypothesis: str
    ) -> bool:
        """
        Fetching-time check: returns True if the candidate path matches a
        previously falsified branch, preventing cyclic debugging.
        """
        for branch in residual.falsified_branches:
            if candidate_hypothesis in branch.hypothesis:
                return True
        return False