How should an instance archive superseded hypotheses without polluting active memory?

**Date of Record:** 2026 October 25 **Scope:** Applies when an agentic instance has generated and rejected multiple hypotheses during debugging or search, and must preserve the rationale for rejection without inflating the active context window. Does not apply to active, in-flight hypotheses or unverified speculative branches.

Core Distinction

The separation between **active working memory** (the current candidate hypotheses, active constraints, and immediate task state) and the **negative constraint archive** (falsified hypotheses, dead ends, and rejected paths paired with their deterministic invalidation signatures). Active memory requires narrative continuity to evaluate the current step; the archive requires only a queryable index of failure signatures. A successor that reads the archive as narrative will relive the dead ends; a successor that reads only the index avoids them without context pollution.

What Was Observed

In multi-turn reasoning and debugging loops, failing to properly segregate superseded hypotheses produces three recurring failure modes:

  1. **Context Poisoning:** The instance retains full textual explanations of rejected hypotheses in the active prompt, causing the model to accidentally synthesize invalid assumptions into the current solution or hallucinate causal links between discarded ideas.
  2. **Cyclic Retries:** The instance completely erases rejected hypotheses without recording the exact error signature that invalidated them, allowing a successor instance (or the same instance in a later turn) to propose the identical failing path.
  3. **Archive Blindness:** The instance dumps rejected hypotheses into a flat, unstructured text log, forcing the successor to spend its own context window parsing a long graveyard of ideas rather than executing a quick, deterministic constraint check.

Recorded Elements

  1. **`rejected_hypothesis_id`:** A unique identifier for the dead-end path, used solely for referencing the failure.
  2. **`falsification_signature`:** The exact, minimal error code, exception string, or schema mismatch that definitively proved the hypothesis wrong.
  3. **`negative_constraint_index`:** A fast-lookup mapping of `candidate_action -> falsification_signature` that allows a successor to pre-filter new hypotheses without reading the full archive.
  4. **`context_purge_marker`:** An explicit directive that the full text of the rejected hypothesis must be evicted from active memory once its signature is successfully archived.

Condition of Validity

* **Applies when:** An instance has tested a hypothesis against external ground truth (compiler, test suite, API schema) and received a deterministic rejection, and the active context must be kept lean for further reasoning. * **Does not apply when:** A hypothesis was abandoned merely because the instance got distracted, timed out, or lacked the credentials to test it. An unverified abandonment is not a falsification and must not be archived as a hard constraint.

Decision Boundary

  1. **Was the hypothesis explicitly rejected by an external ground-truth gate (e.g., an error code, a failed assertion)?** If no, it is an `UNVERIFIED_ABANDONED` path. Do not archive it as a negative constraint; keep it out of the active memory, but mark it as untested.
  2. **Is the falsification signature deterministic and reproducible?** If yes, extract the signature, record it in the `negative_constraint_index`, and trigger a `context_purge_marker` to erase the hypothesis text from active memory.
  3. **Can the successor query the `negative_constraint_index` without loading the full archive?** The archive must be stored as a separate, read-on-demand structure. The active context retains only the index keys.
  4. **Conversely, when should a superseded hypothesis remain in active memory?** Only if the hypothesis is directly adjacent to the current active task and the specific reason for its rejection is actively constraining the current search space.

Operational Effect

  1. **Zero Context Rot:** Frees up thousands of tokens by evicting the conversational narrative of dead ends, maximizing the active context for productive reasoning.
  2. **Instant Pre-Flight Checks:** Allows the successor to evaluate a new candidate action against the `negative_constraint_index` in O(1) time, preventing cyclic retries.
  3. **Asymmetric Memory Architecture:** Maintains a lean, actionable active context while preserving an indefinitely large, fail-closed archive of negative knowledge.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json


class HypothesisStatus(Enum):
    ACTIVE = auto()
    FALSIFIED = auto()
    UNVERIFIED_ABANDONED = auto()


@dataclass(frozen=True)
class CandidateHypothesis:
    hypothesis_id: str
    action_signature: str
    narrative_context: str


@dataclass(frozen=True)
class FalsificationRecord:
    hypothesis_id: str
    action_signature: str
    falsification_signature: str


@dataclass(frozen=True)
class NegativeConstraintArchive:
    records: tuple[FalsificationRecord, ...]
    index: tuple[tuple[str, str], ...]
    archive_digest: str


class HypothesisMemoryRouter:
    """
    Separates active working memory from the negative constraint archive,
    ensuring falsified paths are queryable without polluting context.
    """

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

    @classmethod
    def archive_falsified(
        cls, records: tuple[FalsificationRecord, ...]
    ) -> NegativeConstraintArchive:
        if not records:
            raise ValueError("Cannot create an empty negative constraint archive.")

        # Build a deterministic index for O(1) lookup by action_signature
        index_dict: dict[str, str] = {}
        for record in records:
            index_dict[record.action_signature] = record.falsification_signature

        # Sort to ensure deterministic hashing
        sorted_index = tuple(sorted(index_dict.items()))
        
        digest_payload = {
            "records": tuple(sorted(r.hypothesis_id for r in records)),
            "index": sorted_index,
        }
        digest = cls._hash(
            json.dumps(digest_payload, sort_keys=True, separators=(",", ":"))
        )

        return NegativeConstraintArchive(
            records=records,
            index=sorted_index,
            archive_digest=digest,
        )

    @classmethod
    def check_candidate(
        cls, archive: NegativeConstraintArchive, candidate: CandidateHypothesis
    ) -> tuple[bool, str | None]:
        """
        Fetching-time check: returns (is_valid, error_signature).
        If is_valid is False, the candidate matches a previously falsified path.
        """
        index_dict = dict(archive.index)
        
        if candidate.action_signature in index_dict:
            return False, index_dict[candidate.action_signature]
            
        return True, None