← Context Keep

Where should a long chat forget?

Claude Code can rewind and summarize everything up to a message. Choosing that message is the hard part. It turns out to be a measurable quantity, and most obvious ways to measure it are wrong.

Self-containment · session edd44f35 · 188 messages
Every point is one message you could stop at. The line is how self-contained the kept part would be. Higher is safer to summarize before. The shaded band is the safe zone the tool returns.
The feature

Most people have not found it

Press Esc twice in Claude Code. You get a list of your own messages. Pick one and you are offered, among other things, Summarize up to here.

That does something specific. Everything before the message you picked is replaced by a summary. Everything after it is kept word for word. The exact wording, the file paths, the line numbers, the decision you made three messages ago: all still there.

This is the right shape for coding work. What I am doing at this moment rarely needs the exact words from an hour ago, but it very much needs the exact words from ten minutes ago. A summary of the whole session loses both. A summary of the old half loses neither.

So the feature is good. The problem is the list.

The problem

Which message?

A long session has a hundred and forty messages. The picker shows you five at a time. You scroll, you read half-remembered lines out of context, you pick something that looks like a beginning, and you hope.

I did this many times and I was never sure I had chosen well. Sometimes I summarized away a decision I needed ten minutes later. Sometimes I stopped so late that I saved almost nothing.

The question

Find the latest message i such that everything after i can stand on its own, given only a summary of everything before it.

Latest, because a later stop summarizes more and saves more room. Stands on its own, because that is what makes the summary safe. Both halves of that sentence are measurable.

First attempt

The obvious answer is degenerate

My first instinct, and probably yours, was semantic similarity. Embed the messages. A good place to split is where the two halves are least alike and each half is most alike within itself. Score every split, take the best.

score(i) = coherence(tail) − overlap(head, tail) + λ · compression

I built it. I ran it on a session I had labelled by hand. Then I sorted the candidate stops by score and looked at the order.

It was exactly the tail-size order. Smallest tail first, every time, with no exception. The score was not measuring topic. It was measuring how many messages were left.

TermAs the tail growsRange
coherence, mean similarity to the tail centrefalls0.424 → 0.365
overlap, head centre against tail centrerises0.771 → 0.841
compressionfalls0.85 → 0.27

The compression term was supposed to oppose the coherence term. It does not. A bigger head is a smaller tail, so all three move together and nothing pushes back. Only a hard floor stopped a one-message tail from winning outright.

So I removed the size bias five different ways.

VariantIdeaRank of the right answer, out of 25
Araw centroid coherence − overlap + compression14
Bmean pairwise similarity, no size bias by construction14
Cmean pairwise, compression term removed16
Dtopic dip alone, in the manner of TextTiling12
Ecoherence z-scored against random blocks of equal size19
Ftopic dip plus compression14

Variant B is the one that hurt. Mean pairwise similarity has no size bias at all, by construction, and it still ranked the right answer fourteenth. Variant E removes the size effect statistically and did worst of all.

Why they all fail

Similarity cannot find the start of a piece of work. In that session the right stop was the beginning of a research task. Eight messages later came "create a directory, code and all". That is a stage change inside the same task, and it has a far bigger vocabulary shift than the task's own beginning. Every similarity variant preferred it.

A boundary in meaning and a boundary in dependency are different things. I was measuring the wrong one.

The measure

Ask what the kept part still needs

Stop asking whether the two halves look alike. Ask a narrower question with an unambiguous answer: how much of the vocabulary in the kept part was introduced before the stop?

Take every content word used after the stop. For each use, look up where in the session that word first appeared. If it first appeared after the stop, the kept part brought it in itself. If it first appeared before the stop, the kept part is borrowing from a part you are about to replace with a summary.

self-containment = 1 − (borrowed uses ÷ all uses in the kept part)

A stop with high self-containment is safe. What comes after it barely refers back. A stop with low self-containment is dangerous: the kept messages are full of words whose meaning was established in the part you are dropping.

Two properties make this work where similarity did not. It has no size bias to remove, because it is a ratio over the kept part alone. And it directly measures dependency rather than resemblance, which is the thing the question actually asked about.

It also self-normalises

The words borrowed most often in a long session are ordinary ones: case, check, back, code, test. That looks like a defect, so I tried to remove it four ways: inverse document frequency, its square, and dropping any term that appears in more than 10%, 5% or 3% of messages.

Nothing improved. The zone boundaries did not move by a single message under any setting. The most aggressive cap made it clearly worse, taking the strongest boundary in the test set from 128 times the noise down to 46.

The reason is arithmetic. A common word appears in the dropped part and the kept part alike, so it adds to the borrowed count and the total count at the same time. It cancels in the ratio. The measure was already doing the thing I was trying to add.


Read a real one

Two of my own sessions, measured. Move along the curve to see the message at each stop. The two sessions have different shapes, and the shape is the answer.

The shapes

Three curves, three different answers

A rise of 0.03 means nothing on its own. The same rise is a clear break in a quiet curve and ordinary movement in a restless one. So every candidate zone is measured against the median wiggle of its own curve, and the answer is reported as a multiple of it.

Measured on real sessions: a genuine topic change scores 128 to 191 times the wiggle of its own curve. The sub-topics inside one long piece of work score 6 to 13 times. Both are real signals. Only the first is a break.

That distinction matters more than it sounds. My first version only looked for large steps, so on a long single-topic session it found nothing and said so. That is honest and useless. The session still has to be shortened. Now it reports the sub-topics with their grades and lets me choose, and when even those are absent it shows the trade instead: three stops, each with what it keeps and what it saves.

Output

What it prints

The point of all this is a position number and a quote, so I can find the message in the picker without reading anything.

Every row is one place I could stop. The oldest is at the top, so reading down frees more room. frees is the share of the chat that becomes a summary. shape says what kind of place it is: three dots a clear topic change, two a sub-topic inside the same work, one barely above the noise. A falling mark, ▔▔▁, is the other shape: the curve holds there, then drops away, so the work up to that stop is finished and everything after it leans much harder on the past. The arrow marks the one to take.

  keep  session edd44f35 · 188 messages · 112 places to stop

  self-containment
  0.66 ┤                                         ▃▇█▇▇▆▆▅▆▆▆▇▇
  0.63 ┤                                        ▅│
  0.60 ┤                                        │
  0.57 ┤                                       ▃│
  0.54 ┤▁▁▂▁▁▁▁                        ▁▁      │
  0.51 ┤       ▇▆▅▃▂▁▁                ▄│ ▃▄▂▂▁▂│
  0.48 ┤              ▇▇▇▆▆▇▆▄▂▁▁▁▁ ▃▇│
       └──────────────────────────────────────────────────────
        ········································▔ the pick ▔▔▔
        older → newer

        ↑ 3 older
      scroll to    frees             shape  the message there
      below~82    ▓▓▓▓▌      49%  ●●●   "let me knwo whaen post is published s…"
      below~76    ▓▓▓▓█      54%  ●●●   "ok, stay positive, lets me see what I…"
      below~70    ▓▓▓▓▓▎     58%  ▔▔▁   "ummm, why you think this is good, how…"
      below~56    ▓▓▓▓▓█     65%  ●●●   "ok good we got or today content yeayy"
   →  below~51    ▓▓▓▓▓▓▌    71%  ●●●   "yes agree I continue as I feel dotn w…"

  Esc Esc → scroll to "↓ 51 more below" → "Summarize up to here"
  the note and every stop: file:///…/.claude/context-keep/edd44f35.html

It can also name what to throw away

The same numbers say which part of the dropped half the kept half still leans on. Split the dropped part into six blocks and attribute every borrowed word to the block that introduced it:

rows 0–765.3%
rows 8–1614.8%
rows 17–242.3%side trip
rows 25–324.8%side trip

A block under 5% is a side trip. The later work never refers back to it, so the note tells the summary to drop it whole rather than compress it. That is usually the debugging detour that went nowhere.

The test

Does it agree with a person?

A measure that only satisfies its author is worth nothing. So I built a blind judge: a small model that reads only the user messages of a session, splits them into pieces of work, and chooses where the newest piece began. It never sees the curve, the zones, or any number this tool produces.

Over 19 real sessions, 2,614 user messages, 3 machinesResult
the judge's stop lands inside a zone13 to 15 of 19
inside, or within five messages of one17 to 18 of 19
cost of a full rununder 5 cents, about 20 seconds
An honest caveat

Those are ranges, not numbers, and the ranges are the finding. Three runs of identical code at temperature=0 gave 13, 14 and 15. The harness cannot resolve a difference smaller than about two sessions, so I will not claim a score to one decimal place, and neither should anyone else with a set this size.

The parameter check is firmer. I picked the right stop by hand in a labelled session, then swept 36 combinations of the tuning constants. My stop stayed inside the top zone in all 36. The measure is not balanced on a knife edge, and it agrees with the person whose session it is.

The reading

People already do this

After the measure worked I went looking for why it works, and found that the psychology of memory had described the shape decades ago.

People do not remember experience as a stream. We cut it into events, and we cut it at moments where the situation changes. Zacks and Tversky called these event boundaries, and showed that they are perceived automatically and agreed on across observers.1 That is the same claim as a step in this curve: a place where what follows stops depending on what came before.

Radvansky and Copeland gave it a sharper form. Walk through a doorway and you are measurably worse at recalling what you were holding in mind in the previous room, even when the task is unchanged.2 Crossing a boundary flushes the working detail. Not everything, though: what survives is the point of it.

Brainerd and Reyna's fuzzy-trace theory names the two things separately. Verbatim traces hold the exact form and decay quickly. Gist traces hold the meaning and persist.3 A person leaving a room keeps the gist and drops the verbatim.

The whole tool in one sentence

Find the doorway, keep the verbatim after it, keep only the gist before it.

I want to be careful about the direction of that claim. I did not derive this measure from the literature. I built it, it worked, and then I found that it matches something well described. The reading explains why the shape is right; it is not evidence that the implementation is.

One deliberate consequence of the metaphor: the promise is not that you forget. You carry the gist through the door. The detail is still in the session file on disk, and the next thing I want to build is the way back through: a local store holding the dropped messages, with the summary carrying an id, so the model can ask for a detail the summary dropped instead of losing it.

Use it

A Claude Code plugin. It reads your session file, prints the answer, and changes nothing. No network, no dependencies, Python 3 only.

/plugin marketplace add unclecode/context-keep
/plugin install context-keep@context-keep

Then type /keep in any session. Claude Code does the summarizing; this only says where.

References

  1. Zacks, J. M., & Tversky, B. (2001). Event structure in perception and conception. Psychological Bulletin, 127(1), 3–21.
  2. Radvansky, G. A., & Copeland, D. E. (2006). Walking through doorways causes forgetting: Situation models and experienced space. Memory & Cognition, 34(5), 1150–1156.
  3. Brainerd, C. J., & Reyna, V. F. (2002). Fuzzy-trace theory and false memory. Current Directions in Psychological Science, 11(5), 164–169.
  4. Hearst, M. A. (1997). TextTiling: Segmenting text into multi-paragraph subtopic passages. Computational Linguistics, 23(1), 33–64. The classical form of variant D.