An eviction is a span
An eviction takes one contiguous run of the context — resident turns and
resident residues alike, in id order — and leaves one residue standing in
its place. A residue is a span [from, to] of turn ids, both ends
inclusive, and a note; it has no id of its own and is addressed by the ids it
covers. Residues nest by containment. The context is the frontier — every
turn no residue covers and every residue no later residue covers — in id
order, a residue rendered as one bracketed user-voice message carrying its
note and nothing else. The transcript family is the way back.
exarch-context `evict [from: Int, to: Int, note: Str] requires a note
and may name any closed run, the live prompt included; a forced
eviction is one prefix span, has no note, says so, and may take the live prompt
too. Table order stays
id order, and TURN: N, ids unique and lineage-monotone, are unchanged. What
is given up: a set with a gap is two evictions, not one.
Why
an-eviction-is-a-set-of-turns let a cut take any set of resident turns and left a marker at each hole — a maximal run of departed turns — re-derived at render time from the cuts whose turns lay in it. Three costs followed from the marker being a computation rather than a record:
- It had no address. The model could evict turns but not the marker standing for them, so old notes accumulated with no way to sweep them into one.
- It was not bounded by the note. A marker drew one row per departed turn, forty rows to a hole before collapsing — up to ~3 KB a hole, paid on every request.
- It was re-derived. Holes formed from cut indices at render time are where the inverted-range defect lived; the survivor rule, which kept a prompt while one of its answers stayed, made the sets scattered and the holes ragged.
A span fixes all three at once: it is recorded, it is addressable by numbers the model already knows, and a residue’s size is a constant plus its note.
The decision
- A residue is a record, not a row.
Residue { span, note, by }, exactly what itsProtocol::Evicted { residue }carries, kept in creation order in the table (residues) and never removed.Span { from, to }is one type, never inverted:Span::newrefusesfrom > to, deserialisation goes through it, and itsDisplayis the one spelling,turn 12orturns 12–40. The table of turns stays aVec<Turn>in id order with nothing inserted. Its message,render_residue, is a pure function of the record — the sentence (Turns 41–60 have left your context.), the noteDebug-quoted, and the read-back line — so it is byte-stable. A forced eviction’s residue says it was left in a forced eviction, without a note. No rows:`indexgives what lies under a residue on demand. - Covering, residency, holder, frontier. A residue covers a turn whose
id lies in its span, and an earlier residue whose span lies within its own
(equal spans included: a rewrite covers what it rewrote). A departed item
not withdrawn lies under its holder, the covering residue of smallest
span (
Location::Under(span),`under [from: Int, to: Int]at the surface). Location is stored, a residue’s as a turn’s: an eviction stampsUnderits span on every resident turn and every resident residue it covers, and its own residue stands resident; a withdrawal stampsWithdrawn. The stamp cannot go stale: the residue an item departs under is its holder for good, since any later residue covering the item covers that one whole, and withdrawal is permanent. The frontier is the resident items in id order. Spans are laminar — any two nested or disjoint — by the alignment rule below, which is what makes “smallest” well defined and the resident items disjoint. - One rule the model must learn: a run that reaches into a residue must
cover it whole.
frommust be an item start andtoan item end: no residue hasr.from < from ≤ r.toorr.from ≤ to < r.to. A larger run that covers old residues replaces their notes with the new one; the same span over a residue rewrites its note. Both ends must be recorded and not withdrawn, and the unclosed turn may not lie within. Refusals name the fix (turns 41–60 stand together under one note: name from: 41, to: 60 whole, or leave it). - A withdrawn run may lie inside a span. A
/rewindleaves withdrawn ids mid-sequence, and a forced eviction must span them: a withdrawn leaf inside a span stays withdrawn — withdrawal dominates — and is never rendered. - Inclusive ends.
TURN: Nlets the model writeto: Ndirectly; a leaf isfrom == to; the record, the sentence and the fields agree with no ±1. The one half-open spelling is ral’srange a b, a list constructorreadtakes: the residue’s read-back line and the pressure reminder spell!{range from (to+1)}, and the teaching says so once. - Every row is a span.
SpanRow { span, role, kind, label, bytes, location }(fromandtoat the surface),kind: ItemKindbeingown | import | inherited | residue; a residue’s role isuser, its label its note.`surveyanswers the frontier’s rows;`indexevery item ever recorded, byfrom, wider span first, later-recorded first, so a residue precedes what it covers;`grepsearches turns’ material and model notes (never the boilerplate, never a forced-eviction residue), a hit naming its item byfrom,toandkind, hits in`index’s order.`read [turns: [Int]]is unchanged. - A forced eviction takes one prefix span, and may take the live prompt. Its
plan (
plan_forced_eviction) walks back over frontier items from the newest, which is never a candidate, spendingsuffix_keep_budget; the first item that does not fit ends the span, which runs from the frontier’s first item through it. One ordinaryEvicted, one residue, no note. Nothing in the plan or the fold knows what a prompt is: the live prompt is an item like any other, andresolve_cuthas no case for it. The two authorities differ in one way — the model writes a note and the harness cannot — so the pressure reminder names the span planned as things stand, and a model that wants to keep what was asked evicts first under its own note. - The harness never rewrites. A planned span that would be exactly one residue is no plan, and nothing is evicted: rewriting a note without a note saves nothing. A span over two or more items, residues included, merges them.
- Reversed: the harness once kept the live prompt. An earlier revision of
this decision had the plan step over the live prompt (
current_prompt) and cut two spans around it,[first, p−1]and[p+1, to], so the prompt in hand stood verbatim between two forced-eviction residues. That is withdrawn: the cut never steps over the prompt and never makes two spans. The case the-turn-is-the-atom was written for — a prompt whose work outgrows the window, the task in hand — is now relieved, with the task kept, only by the model’s own eviction. - The survivor and opening rules go. A user-voice residue standing where its run began makes “the frontier opens with a user-role item” true by construction, and a prompt may leave under a note while its answers stay. Contiguity replaces the survivor rule rather than adding to it.
/rewindanchors at an item start.Rewound { anchor }withdraws by id as before; the anchor may not lie strictly inside a resident residue’s span (turn 50 lies under the residue over turns 30–80: rewind to turn 30 or turn 81). So a residue’s leaves are all withdrawn or itsfromis not, and a rewind withdraws every residue whosefromit withdraws.- The link carries residues.
Protocol::Inherited { turns: Vec<Linked>, residues: Vec<Residue> }; the child renders a residue from its own record. The judge checks the residues laminar and every row’slocationconsistent with them —locationis redundant withresidues, and the judge keeps the redundancy honest. The child takes each turn’s location from its row, and derives each residue’s as the fold stamped it: withdrawn with itsfrom, else under the smallest later residue covering it, if any. - Recorded identity. The span is the resolved identity: the leaves it
covers are the recorded ids in it at that point of the log, the residues
it covers the recorded ones within it.
fold(log) == memoholds; old logs do not resume.
What it costs
- Contiguity.
[41, 43]with 42 kept is two evictions. The survivor rule was what produced scattered sets, and it is gone. - The harness may take the task in hand. In a forced eviction the live prompt can leave with the rest under a residue without a note; it stays readable through the transcript, but only the model’s own note keeps what was asked in the context.
- Rewinding to an evicted turn works only at a residue’s
from. - Bounds improve. A residue is ≤ ~400 B of fixed text plus 240 B of note, independent of what it covers; a forced eviction leaves exactly one resident residue.
Provider shape and cache arithmetic are as before: a residue is a user-voice
message between items, never inside a turn, never the last message while a
request is built; a cut rewrites the cache from its from, a note rewrite
from its residue, a forced eviction everything.
Read against UniiChat
UniiChat (Memory.ts, Server.ts, Tools.ts, prompt.txt at 0500bf6)
grows a complete binary tree over every message; a node is one line of at
most 512 bytes written by a separate compactor model, addressed by the span
id+n it covers and nothing else; the view is a frontier sent as text,
older nodes merging by age past a byte threshold; the model opens a node with
zoom(id, n).
Taken:
- Span addressing. A summary needs no identity of its own; the span is
the identity, in the ids the model already reads off
TURN: N. - Nesting by containment. A node over nodes is a bigger span; a residue over residues likewise, with no pointer.
- One bounded line. A residue is its sentence, note and read-back line.
- Children on demand. What lies under a residue is
`index’s to show, never the residue’s.
Not taken:
- The harness does not compress the live prompt. UniiChat compresses nothing of the live turn and lets a giant turn fail, which is the case the-turn-is-the-atom exists for; exarch’s forced eviction is one prefix span and may take the prompt with the rest.
zoom. exarch’s transcript family already reaches everything ever recorded:index,read,grep. No new tag.- The compactor. Evict-don’t-summarise (context-rollover) stands: no provider call in the eviction path, so a forced-eviction residue carries no note.
- History as text. The recent half stays real messages, for the cache and for tool-call pairing.
- The binary tree and age-based merging. The tree exists so a mechanical compactor always has a next job; exarch’s spans are chosen by the model or are the harness’s cache-optimal half-prefix, and a laminar family of intervals is all the structure needed.
Supersedes
- an-eviction-is-a-set-of-turns:
“a cut is a set” becomes a cut is a run; §3 (the survivor rule and the
emptied-set refusal), §4 (a marker per hole), §6’s hit shape, and §8’s
marker sentence and
TurnRowshape. - the-turn-is-the-atom: its
marker and
Held { cut }carriers. “One id space” stands, untouched. - context-rollover: the head marker’s
HEAD_ROWSbounds paragraph.
See also
session-record (the table, the frontier, the
render), agent (the pressure plan and the reminder),
builtins (exarch-context and exarch-transcript
as the model reads them),
transcript-admission (where a residue
may stand).