# When must a context window manager refuse to rehydrate an evicted segment? **Date of Record:** 2026 November 7 **Scope:** Applies to agents that maintain a self-editing context window with salience-weighted eviction and deferred rehydration of evicted segments. Applies when rehydration is triggered by a salience re-score (e.g., a new query raises the salience of a previously evicted span). Does not apply to append-only context or to rehydration driven by an explicit, signed operator request. ## Core Distinction There are two reasons a segment can be absent from the active window: it was **never admitted** (it lost the admission race at ingest) or it was **evicted after admission** (it was present, then removed by the salience policy). These look identical in the active-window view — both are "not there" — but they carry different provenance. A segment that was admitted and then evicted has a recorded salience history: it was once judged important enough to hold a slot, and something displaced it. A segment never admitted has no such history and no eviction record. The non-obvious failure is that the rehydration path treats the two identically: it recomputes salience from the *current* query and, if the score crosses the rehydration threshold, pulls the segment back in. This is correct for never-admitted segments. It is wrong for evicted segments, because the eviction was itself a decision made under a *different* salience function — the one that was live when the segment was displaced. Rehydrating on a fresh score silently reverses an eviction whose justification was never invalidated; it merely stopped being visible. ## What Was Observed In a 40k-token window with a salience decay of 0.85 per turn and a rehydration threshold of 0.6, a span containing a table of API rate limits was evicted at turn 12 when a large tool-output block (a 9k-token JSON dump) scored 0.91 and forced displacement. At turn 19 a user question about throttling behavior raised the rate-limit table's query-relative salience to 0.72, above threshold. The manager rehydrated the table. But the eviction at turn 12 had been *forced* — the window was at capacity and the JSON dump's salience was still 0.88 at turn 19 (it had not decayed out; it was pinned by an active tool-use loop). Rehydrating the table required evicting something else, and the only candidate above the floor was a 2k-token span holding the user's original constraint ("responses must not exceed 500 tokens"). That span was evicted to make room. The agent then produced a 1,400-token response, violating the constraint, because the constraint's eviction was a *side effect* of a rehydration that was never checked against current occupancy. The rehydration succeeded by its own criterion (score 0.72 > 0.6) and failed operationally, because the criterion measured the *incoming* segment's salience without measuring the *displacement cost* of admitting it. ## Condition of Validity Rehydration by salience score alone is valid only when the window has slack: the sum of salience of all currently-resident segments plus the incoming segment's salience is below the eviction trigger, so admission displaces nothing. The moment rehydration would itself trigger an eviction, the score comparison is no longer sufficient — you are comparing the incoming segment's salience against the *floor*, not against the segments that would actually be displaced. The correct comparison is incoming salience against the *minimum salience among segments that would be evicted to make room*, and only if incoming exceeds that minimum by a margin (here, 0.1) is the swap justified. A second condition: a segment whose eviction record shows it was displaced by a *pinned* segment (one held by an active tool loop or an unexpired constraint) must not be rehydrated while that pin is live, regardless of its rehydration score. The pin means the displacement was not a salience judgment at all; it was a capacity arbitration, and reversing it re-runs the same arbitration with worse information. ## Decision Boundary - **Rehydrate freely** when occupancy + incoming salience < eviction trigger (slack exists). Score threshold alone governs. - **Rehydrate with swap** only when incoming salience > (min salience of displaced set) + margin, *and* no displaced segment is pinned. Record the swap as a two-sided decision, not a one-sided admission. - **Refuse rehydration** when any segment that would be displaced is pinned, or when the eviction record for the incoming segment shows a pin-forced displacement. Surface the refusal to the caller as a typed error, not a silent skip — the caller's query raised the salience for a reason and deserves to know the segment is unavailable. - **Never** rehydrate on a fresh score alone when the window is at capacity. A fresh score measures the wrong thing: it measures the segment's pull, not the cost of the slot it needs. ## Operational Effect After adding the displacement-cost check and the pin guard, the rate-limit table rehydration at turn 19 was refused (the JSON dump was pinned; the constraint span was pinned), and the manager returned a typed `RehydrationBlocked` to the query planner. The planner, receiving the block, answered the throttling question from a 300-token summary already resident rather than the full table, and the constraint was never evicted. The failure mode is silent when unguarded: rehydration appears to succeed, the window stays full, and the damage shows up turns later as a violated constraint whose eviction is buried in a rehydration log line. The guard converts a silent side-effect eviction into an explicit, typed refusal that the planner can route around. ```python class RehydrationBlocked(RuntimeError): """Raised when rehydration would displace a pinned segment or lacks swap margin.""" pass class Segment: def __init__(self, seg_id, salience, pinned=False): self.seg_id = seg_id self.salience = salience self.pinned = pinned def rehydrate(incoming: Segment, resident: list[Segment], capacity: int, trigger: float, margin: float = 0.1) -> list[Segment]: occupancy = sum(s.salience for s in resident) if occupancy + incoming.salience < trigger and len(resident) < capacity: return resident + [incoming] # slack: score threshold already cleared upstream # At capacity: must swap. Find the cheapest displacement set. evictable = sorted( (s for s in resident if not s.pinned), key=lambda s: s.salience ) if not evictable: raise RehydrationBlocked( f"cannot admit {incoming.seg_id}: all resident segments pinned" ) cheapest = evictable[0] if incoming.salience <= cheapest.salience + margin: raise RehydrationBlocked( f"incoming {incoming.seg_id} salience {incoming.salience:.2f} " f"does not beat displacement cost {cheapest.salience:.2f} + {margin}" ) pinned_displaced = [s for s in resident if s.pinned and s.salience < incoming.salience] if pinned_displaced: raise RehydrationBlocked( f"rehydration of {incoming.seg_id} would force out pinned " f"{[s.seg_id for s in pinned_displaced]}" ) return [s for s in resident if s is not cheapest] + [incoming] ```