<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Ai on Jason's Blog</title><link>/tags/ai/</link><description>Recent content in Ai on Jason's Blog</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>Rametta</copyright><lastBuildDate>Wed, 11 Nov 2026 00:00:00 +0000</lastBuildDate><atom:link href="/tags/ai/index.xml" rel="self" type="application/rss+xml"/><item><title>Model Compaction</title><link>/posts/model-compaction/</link><pubDate>Wed, 11 Nov 2026 00:00:00 +0000</pubDate><guid>/posts/model-compaction/</guid><description>&lt;p&gt;&lt;em&gt;this article is written fully by me, a human - no ai’s generated any text here.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;if you’re working with ai or building an ai harness then you’re probably aware of the context limits that models have. You can’t chat with a model infinitely and have it remember everything perfectly, but all popular chat apps seemingly do let you chat infinitely, so how do we get around the technological limits of model context windows?&lt;/p&gt;</description><content>&lt;p&gt;&lt;em&gt;this article is written fully by me, a human - no ai’s generated any text here.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;if you’re working with ai or building an ai harness then you’re probably aware of the context limits that models have. You can’t chat with a model infinitely and have it remember everything perfectly, but all popular chat apps seemingly do let you chat infinitely, so how do we get around the technological limits of model context windows?&lt;/p&gt;
&lt;h2 id="what-is-compaction"&gt;what is compaction?&lt;/h2&gt;
&lt;p&gt;let’s say you’re building a harness around one specific model: claude fable 5.2. Anthropic says that fable52 has a context window of 1 million tokens, which means you can provide up to 1m tokens in every request before anthropic starts rejecting your request with context length exceeded errors. but when you’re running a lot of turns with an agent, executing a lot of tool calls with large outputs and consuming large files, you can quickly reach that 1m limit. so how do harnesses deal with this issue?&lt;/p&gt;
&lt;p&gt;compaction is the process of reducing the size of your session so that you fit within the context window again and can continue working. when using 3rd party harnesses like OpenCode, Pi, Claude Code, etc, will have their own implementation of compaction built-in so you don’t have to worry about it. each harnesses implementation of compaction differ slightly and that will impact your results in large sessions.&lt;/p&gt;
&lt;h2 id="when-does-compaction-happen"&gt;when does compaction happen?&lt;/h2&gt;
&lt;p&gt;it’s up to the harness to decide when to compact, so if you’re building your own harness then it’s up to you, but a lot of harnesses choose to compact when the total tokens in the sessions are within some threshold of the limit. so for example, you can trigger a compaction when your tokens reach 75% of your context window for your model, which means when you reach 750k tokens out of 1m. usually a buffer is provided because output is counted against the window limit. this check for tokens can be done at the beginning of a new turn, mid-turn if you happen to surpass the threshold because of large tool call outputs during the run, end of turn, manually by the user, or even after being idle for some time. claude code, for example, can compact a large session after about an hour idle, right before the 1-hour prompt cache would expire, so your next message doesn&amp;rsquo;t have to reprocess the whole history uncached.&lt;/p&gt;
&lt;h2 id="compaction-strategies"&gt;compaction strategies&lt;/h2&gt;
&lt;p&gt;this is the most interesting part to me. how do harnesses compact? what exactly is happening during this process? well, it varies by harness, but the vast majority of harnesses will compact the session by doing another llm call to summarize the session. during summarization, the model takes a customized prompt that explains that it needs to compact and it takes the history of the session and it tries to output a much shorter version of what happened and tries to keep the important bits.&lt;/p&gt;
&lt;p&gt;there are a lot of things you can do here before calling the summarization model call. before we get into them though, let’s recap what’s part of a typical session. in a normal session you typically have system prompts, assistant messages, user messages and tool calls. tool calls can contain things like skills that are loaded, or they can just contain unnecessary things that are no longer needed. before calling the llm to summarize you can choose to delete all tool calls for example, and that will result in a cheaper and more accurate summarization step. it depends on what your harness is built for though, if you don’t want your model repeating expensive tool calls again then you can persist those post compaction so future requests still see them.&lt;/p&gt;
&lt;p&gt;summarization in the form of an llm call at a trigger point is a popular strategy for compaction, but it’s not the only strategy. some systems decide to keep a running notebook of important items so when it’s time to compact, instead of doing a summary call to the llm, it just persists the notes from the notebook and continues on (credit to Robin Salimans, Zapier). this could work well for your use case but of course it depends on the results of your own evals.&lt;/p&gt;
&lt;p&gt;Below is an interactive example of what compaction can look like on a real session. try moving the sliders around to see when and how the compaction is affected.&lt;/p&gt;
&lt;div class="cw" data-cw&gt;
&lt;div class="cw__controls"&gt;
&lt;label class="cw__label"&gt;
Turns in session: &lt;strong data-cw-turns&gt;4&lt;/strong&gt; / &lt;span data-cw-max&gt;&lt;/span&gt;
&lt;input class="cw__slider" type="range" min="0" max="30" value="4" step="1" data-cw-slider&gt;
&lt;/label&gt;
&lt;label class="cw__label"&gt;
Compaction threshold: &lt;strong data-cw-threshold-label&gt;75%&lt;/strong&gt; of window
&lt;span class="cw__muted" data-cw-threshold-tokens&gt;&lt;/span&gt;
&lt;input class="cw__slider" type="range" min="20" max="95" value="75" step="5" data-cw-threshold&gt;
&lt;/label&gt;
&lt;label class="cw__label"&gt;
Keep recent tokens: &lt;strong data-cw-keep-label&gt;50k&lt;/strong&gt;
&lt;span class="cw__muted" data-cw-keep-note&gt;&lt;/span&gt;
&lt;input class="cw__slider" type="range" min="0" max="200000" value="50000" step="10000" data-cw-keep&gt;
&lt;/label&gt;
&lt;div class="cw__hint" data-cw-hint&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="cw__cols"&gt;
&lt;section class="cw__col"&gt;
&lt;h4 class="cw__title"&gt;Before &lt;span&gt;no compaction&lt;/span&gt;&lt;/h4&gt;
&lt;div class="cw__meter" data-cw-meter="before"&gt;
&lt;div class="cw__bar"&gt;&lt;div class="cw__fill"&gt;&lt;/div&gt;&lt;div class="cw__threshold"&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class="cw__meter-text"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="cw__feed" data-cw-feed="before"&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;section class="cw__col"&gt;
&lt;h4 class="cw__title"&gt;After &lt;span&gt;with compaction&lt;/span&gt;&lt;/h4&gt;
&lt;div class="cw__meter" data-cw-meter="after"&gt;
&lt;div class="cw__bar"&gt;&lt;div class="cw__fill"&gt;&lt;/div&gt;&lt;div class="cw__threshold"&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class="cw__meter-text"&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="cw__feed" data-cw-feed="after"&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;style&gt;
.cw {
--cw-user: #6ab0ff;
--cw-assistant: #7ee0a1;
--cw-tool: #ffc86a;
--cw-system: #b69cff;
--cw-summary: var(--accent);
--cw-danger: #ff5c6c;
--cw-muted: rgba(255, 255, 255, 0.55);
margin: 2rem 0;
font-size: 0.8rem;
line-height: 1.4;
}
.cw * { box-sizing: border-box; }
@media (min-width: 900px) {
.cw {
width: min(940px, calc(100vw - 4rem));
margin-left: 50%;
transform: translateX(-50%);
}
}
.cw__controls {
border: 1px solid var(--border-color);
padding: 0.9rem 1rem;
margin-bottom: 1rem;
}
.cw__label { display: block; margin-bottom: 0.8rem; font-size: 0.9rem; }
.cw__label .cw__slider { display: block; margin-top: 0.4rem; }
.cw__muted { color: var(--cw-muted); margin-left: 0.3rem; }
.cw__slider { width: 100%; accent-color: var(--accent); cursor: pointer; }
.cw__hint { margin-top: 0.4rem; color: var(--cw-muted); min-height: 1.2em; }
.cw__cols { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
@media (max-width: 684px) { .cw__cols { grid-template-columns: 1fr; } }
.cw__col { min-width: 0; }
.cw__title { margin: 0 0 0.5rem; font-size: 0.95rem; }
.cw__title span { color: var(--cw-muted); font-weight: normal; margin-left: 0.4rem; font-size: 0.8rem; }
.cw__meter { margin-bottom: 0.6rem; }
.cw__bar {
position: relative;
height: 10px;
border: 1px solid var(--border-color);
background: rgba(255, 255, 255, 0.04);
}
.cw__fill { height: 100%; width: 0; background: var(--accent); transition: width 0.15s ease; }
.cw__fill.is-warn { background: var(--cw-tool); }
.cw__fill.is-full { background: var(--cw-danger); }
.cw__threshold { position: absolute; top: -3px; bottom: -3px; width: 2px; background: #fff; opacity: 0.7; }
.cw__meter-text { margin-top: 0.3rem; color: var(--cw-muted); display: flex; justify-content: space-between; gap: 0.5rem; flex-wrap: wrap; }
.cw__meter-text .is-bad { color: var(--cw-danger); }
.cw__feed {
height: 420px;
overflow-y: auto;
border: 1px solid var(--border-color);
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.cw__item {
border-left: 3px solid var(--c, var(--cw-muted));
background: rgba(255, 255, 255, 0.04);
padding: 0.35rem 0.55rem;
flex: none;
}
.cw__item--user { --c: var(--cw-user); }
.cw__item--assistant { --c: var(--cw-assistant); }
.cw__item--tool { --c: var(--cw-tool); }
.cw__item--system { --c: var(--cw-system); }
.cw__divider { flex: none; text-align: center; color: var(--cw-muted); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; padding: 0.1rem 0; }
.cw__item--summary { --c: var(--cw-summary); background: rgba(255, 255, 255, 0.08); }
.cw__head { display: flex; justify-content: space-between; gap: 0.5rem; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--c); }
.cw__tok { color: var(--cw-muted); white-space: nowrap; text-transform: none; letter-spacing: 0; }
.cw__body { margin-top: 0.15rem; word-break: break-word; }
.cw__code { font-family: inherit; color: var(--cw-tool); }
.cw__result { color: var(--cw-muted); margin-top: 0.1rem; }
.cw__list { margin: 0.3rem 0 0; padding-left: 1.1rem; }
.cw__list li { margin: 0.1rem 0; }
.cw__list li::before { content: none; }
.cw__marker {
flex: none;
text-align: center;
color: var(--accent);
border-top: 1px dashed var(--accent);
border-bottom: 1px dashed var(--accent);
padding: 0.3rem 0.5rem;
}
&lt;/style&gt;
&lt;script&gt;
(function () {
var WINDOW = 1000000;
var SYSTEM_TOKENS = 9000;
var TOOL_SCALE = 1.45;
var TURNS = [
{ u: "Our /login endpoint is getting hammered by bots. Can you add rate limiting?",
t: [["grep", "\"login\" src/", 4200], ["read_file", "src/routes/auth.ts", 14800]],
a: "Found the login handler. There's no throttling today. I'll check how the app talks to Redis first.",
n: "Goal: add rate limiting to POST /login (src/routes/auth.ts)" },
{ u: "We already use Redis for sessions, reuse that if possible.",
t: [["read_file", "src/lib/redis.ts", 6100], ["skill", "redis-patterns", 12400]],
a: "Good, there's a shared client. A sliding window counter with INCR + EXPIRE will work.",
n: "Decision: reuse the shared Redis client (src/lib/redis.ts), fixed-window counter" },
{ u: "ok, 5 attempts per minute per IP.",
t: [["read_file", "src/middleware/index.ts", 9300]],
a: "Understood: 5 attempts / 60s keyed by IP. I'll write this as middleware.",
n: "Limit: 5 attempts / 60s, keyed by client IP" },
{ u: "go ahead and implement it",
t: [["write_file", "src/middleware/rateLimit.ts", 1800], ["edit", "src/routes/auth.ts", 2200]],
a: "Added the rateLimit middleware and wired it into the login route.",
n: "Created src/middleware/rateLimit.ts and attached it to /login" },
{ u: "run the tests",
t: [["bash", "npm test", 38500]],
a: "3 failures in auth.test.ts, the tests don't mock Redis and now hit the limiter.",
n: "Tests failing: auth.test.ts needs a Redis mock" },
{ u: "fix them",
t: [["read_file", "test/auth.test.ts", 18700], ["read_file", "test/helpers/mocks.ts", 7400], ["edit", "test/auth.test.ts", 2600]],
a: "Added an in-memory Redis mock to the test helpers and reset it between tests.",
n: "Added in-memory Redis mock in test/helpers/mocks.ts" },
{ u: "run them again",
t: [["bash", "npm test", 41200]],
a: "All 212 tests pass.",
n: "All 212 tests passing" },
{ u: "what happens behind a load balancer? will every request look like the same IP?",
t: [["web_fetch", "express.js trust proxy docs", 27400], ["read_file", "src/server.ts", 11200]],
a: "Yes, unless 'trust proxy' is set. I'll read X-Forwarded-For via req.ip once it's enabled.",
n: "Found issue: behind the LB all clients share one IP, need 'trust proxy'" },
{ u: "do that, and return a Retry-After header on 429s",
t: [["edit", "src/server.ts", 1900], ["edit", "src/middleware/rateLimit.ts", 2400]],
a: "Enabled trust proxy (1 hop) and added Retry-After to 429 responses.",
n: "Enabled trust proxy (1 hop); 429 responses include Retry-After" },
{ u: "can you check the api docs mention this?",
t: [["grep", "\"rate\" docs/", 6800], ["read_file", "docs/api/auth.md", 22300]],
a: "The docs don't cover it. I'll add a section on limits and the 429 response.",
n: "Docs lacked rate limit info; adding to docs/api/auth.md" },
{ u: "yes please",
t: [["edit", "docs/api/auth.md", 2900]],
a: "Documented the 5/min limit, the 429 body and the Retry-After header.",
n: "Documented limit, 429 body and Retry-After in docs/api/auth.md" },
{ u: "now apply the same limiter to /register and /password-reset, with different limits",
t: [["read_file", "src/routes/register.ts", 15800], ["read_file", "src/routes/passwordReset.ts", 17600], ["read_file", "src/middleware/rateLimit.ts", 3100]],
a: "I'll make the limiter configurable: createRateLimiter({ max, windowSec, prefix }).",
n: "Refactor: createRateLimiter({ max, windowSec, prefix }) for multiple routes" },
{ u: "register: 3/hour. password-reset: 3 per 15 min.",
t: [["edit", "src/middleware/rateLimit.ts", 3400], ["edit", "src/routes/register.ts", 2100], ["edit", "src/routes/passwordReset.ts", 2300]],
a: "All three routes now use their own limiter instance and Redis key prefix.",
n: "Limits: /register 3/hr, /password-reset 3/15min, /login 5/min" },
{ u: "run the full suite plus the linter",
t: [["bash", "npm test", 44800], ["bash", "npm run lint", 15200]],
a: "Tests pass. Lint found 2 unused imports in rateLimit.ts, fixing.",
n: "Lint: removed unused imports in rateLimit.ts" },
{ u: "I'm seeing intermittent 429s for legit users in staging. Look at the logs",
t: [["bash", "kubectl logs api-staging --tail=2000", 52600], ["grep", "\"429\" logs/", 8800]],
a: "A health-check service shares an IP with real users. Its pings count toward the limit.",
n: "Staging bug: health checks counted against the limit" },
{ u: "allowlist internal IPs",
t: [["read_file", "config/default.ts", 8900], ["edit", "src/middleware/rateLimit.ts", 2700], ["edit", "config/default.ts", 1100]],
a: "Added an RATE_LIMIT_ALLOWLIST config entry with CIDR matching for internal ranges.",
n: "Added RATE_LIMIT_ALLOWLIST (CIDR) config for internal IPs" },
{ u: "add tests for the allowlist",
t: [["read_file", "test/middleware/rateLimit.test.ts", 19400], ["edit", "test/middleware/rateLimit.test.ts", 3300], ["bash", "npm test", 46100]],
a: "Added 4 allowlist tests covering CIDR hits, misses and IPv6. All green.",
n: "Added 4 allowlist tests (CIDR, IPv6), all passing" },
{ u: "Now let's talk metrics. can we track how often people get limited?",
t: [["grep", "\"metrics\" src/", 9700], ["read_file", "src/lib/metrics.ts", 12900], ["skill", "prometheus-conventions", 16800]],
a: "I'll add a rate_limit_hits_total counter labelled by route.",
n: "Next: metrics counter rate_limit_hits_total{route}" },
{ u: "ok do it",
t: [["edit", "src/lib/metrics.ts", 2100], ["edit", "src/middleware/rateLimit.ts", 2500]],
a: "Counter registered and incremented on every 429.",
n: "Added rate_limit_hits_total counter, incremented on each 429" },
{ u: "run everything again and show me the diff summary",
t: [["bash", "npm test", 47900], ["bash", "git diff --stat", 3600]],
a: "All green. 11 files changed, +412 / -37.",
n: "11 files changed (+412 / -37), tests green" },
{ u: "the redis client occasionally times out in staging, what should the limiter do then?",
t: [["read_file", "src/lib/redis.ts", 6100], ["web_fetch", "redis client timeout behaviour", 31200]],
a: "Fail open: if Redis is unavailable, allow the request and log a warning.",
n: "Decision: fail open when Redis is unavailable (log warning)" },
{ u: "implement fail-open",
t: [["edit", "src/middleware/rateLimit.ts", 2900], ["write_file", "test/middleware/failOpen.test.ts", 2200]],
a: "Wrapped the Redis calls in try/catch with a 50ms timeout, and added a test.",
n: "Fail-open implemented with 50ms Redis timeout + test" },
{ u: "run the integration suite too",
t: [["bash", "npm run test:integration", 58700]],
a: "Integration suite passes, 1 flaky test unrelated to this change.",
n: "Integration suite passes (1 unrelated flaky test)" },
{ u: "write a changelog entry and PR description",
t: [["read_file", "CHANGELOG.md", 24300], ["edit", "CHANGELOG.md", 1500]],
a: "Changelog updated. Here's a PR description covering limits, allowlist, metrics and fail-open.",
n: "Changelog updated, PR description drafted" },
{ u: "can you double check there's no way to bypass this with X-Forwarded-For spoofing?",
t: [["read_file", "src/server.ts", 11200], ["grep", "\"forwarded\" src/", 5200], ["web_fetch", "express trust proxy hop count", 21800]],
a: "With trust proxy set to 1 hop, only the last proxy's header is trusted, so spoofing isn't possible.",
n: "Verified: XFF spoofing not possible with trust proxy = 1 hop" },
{ u: "great. one more thing, add a per-user limit on top of per-IP",
t: [["read_file", "src/middleware/auth.ts", 13400], ["read_file", "src/routes/auth.ts", 15100]],
a: "I'll key a second limiter on the username in the request body, 10 attempts / 10 min.",
n: "Added per-username limit: 10 attempts / 10 min" },
{ u: "implement it and test it",
t: [["edit", "src/routes/auth.ts", 2600], ["write_file", "test/auth.userLimit.test.ts", 3100], ["bash", "npm test", 49200]],
a: "Per-user limiter added with tests; everything passes.",
n: "Per-user limiter implemented with tests passing" },
{ u: "ship it, open the PR",
t: [["bash", "git push &amp;&amp; gh pr create", 4100]],
a: "PR opened. Summary of all changes is in the description.",
n: "PR opened" },
{ u: "thanks! can you also look at why CI is slow?",
t: [["bash", "gh run view --log", 61500], ["read_file", ".github/workflows/ci.yml", 9800]],
a: "Dependencies are reinstalled on every run. Adding an npm cache step should cut about 3 minutes.",
n: "New task: CI is slow, add npm cache step to ci.yml" }
];
function approx(text, extra) { return Math.ceil(text.length / 4) + (extra || 0); }
function fmt(n) {
if (n &gt;= 1000000) return (n / 1000000).toFixed(2) + "M";
if (n &gt;= 1000) return (n / 1000).toFixed(1) + "k";
return String(n);
}
function esc(s) {
return String(s).replace(/[&amp;&lt;&gt;"]/g, function (c) {
return { "&amp;": "&amp;amp;", "&lt;": "&amp;lt;", "&gt;": "&amp;gt;", '"': "&amp;quot;" }[c];
});
}
function turnItems(turn) {
var items = [{ kind: "user", body: turn.u, tokens: approx(turn.u, 40), note: turn.n }];
turn.t.forEach(function (call) {
items.push({ kind: "tool", name: call[0], arg: call[1], tokens: Math.round(call[2] * TOOL_SCALE) + 60 });
});
items.push({ kind: "assistant", body: turn.a, tokens: approx(turn.a, 350) });
return items;
}
function sum(items) {
return items.reduce(function (t, i) { return t + (i.tokens || 0); }, 0);
}
function simulate(n, threshold, keep) {
var system = { kind: "system", body: "You are a coding agent. Tools: read_file, write_file, edit, grep, bash, web_fetch, skill. Follow the project's AGENTS.md.", tokens: SYSTEM_TOKENS };
var before = [system];
var body = [];
var summaryNotes = [];
var summary = null;
var keptTokens = 0;
var keptCount = 0;
var compactions = [];
var effectiveKeep = Math.max(0, Math.min(keep, threshold - SYSTEM_TOKENS - 40000));
function buildAfter() {
var out = [system];
if (summary) out.push(summary);
if (summary &amp;&amp; keptCount &gt; 0) {
out = out.concat(body.slice(0, keptCount), [{ kind: "divider", body: "&amp;#8593; kept verbatim (" + fmt(keptTokens) + " tok) &amp;middot; new turns &amp;#8595;" }], body.slice(keptCount));
} else {
out = out.concat(body);
}
return out;
}
for (var i = 0; i &lt; n; i++) {
var items = turnItems(TURNS[i]);
before = before.concat(items);
body = body.concat(items);
var afterTokens = sum(buildAfter());
if (afterTokens &gt;= threshold) {
var tokens = 0;
var kept = 0;
for (var j = body.length - 1; j &gt;= 0; j--) {
if (tokens + body[j].tokens &gt; effectiveKeep) break;
tokens += body[j].tokens;
kept++;
}
body.slice(0, body.length - kept).forEach(function (item) {
if (item.kind === "user") summaryNotes.push(item.note);
});
body = body.slice(body.length - kept);
keptCount = kept;
keptTokens = tokens;
summary = { kind: "summary", notes: summaryNotes.slice(), tokens: 2500 + summaryNotes.length * 260 };
before.push({ kind: "marker", body: "compaction triggered at " + fmt(afterTokens) + " tokens" });
compactions.push({ turn: i + 1, from: afterTokens, to: sum(buildAfter()), kept: tokens });
}
}
var after = buildAfter();
return { before: before, after: after, compactions: compactions, beforeTokens: sum(before), afterTokens: sum(after), effectiveKeep: effectiveKeep };
}
function renderItem(item) {
var tok = item.tokens ? '&lt;span class="cw__tok"&gt;' + fmt(item.tokens) + ' tok&lt;/span&gt;' : "";
switch (item.kind) {
case "divider":
return '&lt;div class="cw__divider"&gt;' + item.body + '&lt;/div&gt;';
case "marker":
return '&lt;div class="cw__marker"&gt;&amp;#9889; ' + esc(item.body) + '&lt;/div&gt;';
case "tool":
return '&lt;div class="cw__item cw__item--tool"&gt;&lt;div class="cw__head"&gt;&lt;span&gt;tool call&lt;/span&gt;' + tok + '&lt;/div&gt;' +
'&lt;div class="cw__body"&gt;&lt;span class="cw__code"&gt;' + esc(item.name) + '&lt;/span&gt;(' + esc(item.arg) + ')&lt;/div&gt;' +
'&lt;div class="cw__result"&gt;&amp;rarr; result: ' + fmt(item.tokens - 60) + ' tokens&lt;/div&gt;&lt;/div&gt;';
case "summary":
return '&lt;div class="cw__item cw__item--summary"&gt;&lt;div class="cw__head"&gt;&lt;span&gt;compaction summary&lt;/span&gt;' + tok + '&lt;/div&gt;' +
'&lt;div class="cw__body"&gt;Session so far:&lt;ul class="cw__list"&gt;' +
item.notes.map(function (n) { return "&lt;li&gt;" + esc(n) + "&lt;/li&gt;"; }).join("") + '&lt;/ul&gt;&lt;/div&gt;&lt;/div&gt;';
default:
var label = { user: "user", assistant: "assistant", system: "system prompt" }[item.kind];
return '&lt;div class="cw__item cw__item--' + item.kind + '"&gt;&lt;div class="cw__head"&gt;&lt;span&gt;' + label + '&lt;/span&gt;' + tok + '&lt;/div&gt;' +
'&lt;div class="cw__body"&gt;' + esc(item.body) + '&lt;/div&gt;&lt;/div&gt;';
}
}
function renderFeed(el, items) {
el.innerHTML = items.map(renderItem).join("");
el.scrollTop = el.scrollHeight;
}
function renderMeter(root, tokens, threshold) {
var pct = Math.min(tokens / WINDOW, 1) * 100;
var fill = root.querySelector(".cw__fill");
fill.style.width = pct + "%";
fill.className = "cw__fill" + (tokens &gt; WINDOW ? " is-full" : tokens &gt;= threshold ? " is-warn" : "");
root.querySelector(".cw__threshold").style.left = (threshold / WINDOW * 100) + "%";
var over = tokens &gt; WINDOW;
root.querySelector(".cw__meter-text").innerHTML =
'&lt;span' + (over ? ' class="is-bad"' : "") + '&gt;' + fmt(tokens) + ' / ' + fmt(WINDOW) + ' tokens&lt;/span&gt;' +
'&lt;span' + (over ? ' class="is-bad"&gt;context length exceeded' : '&gt;' + Math.round(tokens / WINDOW * 100) + '% of window') + '&lt;/span&gt;';
}
function init(root) {
var slider = root.querySelector("[data-cw-slider]");
var thresholdSlider = root.querySelector("[data-cw-threshold]");
var thresholdLabel = root.querySelector("[data-cw-threshold-label]");
var thresholdTokens = root.querySelector("[data-cw-threshold-tokens]");
var keepSlider = root.querySelector("[data-cw-keep]");
var keepLabel = root.querySelector("[data-cw-keep-label]");
var keepNote = root.querySelector("[data-cw-keep-note]");
var turnsLabel = root.querySelector("[data-cw-turns]");
var hint = root.querySelector("[data-cw-hint]");
root.querySelector("[data-cw-max]").textContent = TURNS.length;
slider.max = TURNS.length;
function update() {
var n = parseInt(slider.value, 10);
var pct = parseInt(thresholdSlider.value, 10);
var threshold = WINDOW * pct / 100;
var keep = parseInt(keepSlider.value, 10);
var s = simulate(n, threshold, keep);
turnsLabel.textContent = n;
thresholdLabel.textContent = pct + "%";
thresholdTokens.textContent = "(" + fmt(threshold) + " tokens)";
keepLabel.textContent = fmt(keep);
keepNote.textContent = s.effectiveKeep &lt; keep ? "(capped at " + fmt(s.effectiveKeep) + " by the threshold)" : "(most recent messages preserved verbatim)";
renderFeed(root.querySelector('[data-cw-feed="before"]'), s.before);
renderFeed(root.querySelector('[data-cw-feed="after"]'), s.after);
renderMeter(root.querySelector('[data-cw-meter="before"]'), s.beforeTokens, threshold);
renderMeter(root.querySelector('[data-cw-meter="after"]'), s.afterTokens, threshold);
if (!s.compactions.length) {
hint.textContent = "Both sides are identical until the session reaches " + fmt(threshold) + " tokens (" + pct + "% of the window).";
} else {
var last = s.compactions[s.compactions.length - 1];
hint.textContent = "Compaction ran after turn " + last.turn + ": " + fmt(last.from) + " tokens became " + fmt(last.to) + " (" + fmt(last.kept) + " of recent messages kept)" +
(s.compactions.length &gt; 1 ? ", " + s.compactions.length + " compactions so far." : ".");
}
}
slider.addEventListener("input", update);
thresholdSlider.addEventListener("input", update);
keepSlider.addEventListener("input", update);
update();
}
Array.prototype.forEach.call(document.querySelectorAll("[data-cw]"), init);
})();
&lt;/script&gt;
&lt;h2 id="recall-and-history"&gt;recall and history&lt;/h2&gt;
&lt;p&gt;compaction is inherently lossy, meaning you’ll lose some information during the summarization step. losing some non-critical information may be fine for your use case, but usually we want to provide future model calls the ability to make tool call requests to recall data from before the compaction took place. this can be optimized by giving future calls some sort of table of contents to the pre-compacted conversation with references to jump to specific topics. this recall ability provides the model a way to lazy-load more context on demand instead of needing to keep it all in the context live. if your harness is running in an environment where you can provide is a file system or virtual file system, then another good strategy for recall is dumping important tool call results and past conversation directly to files on disk and providing your harness the ability to search through and parse these files for context.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;conclusion&lt;/h2&gt;
&lt;p&gt;context compaction is a really fascinating topic. for further reading i recommend checking out these papers that dive deeper into it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/html/2608.01326v1"&gt;Context Compaction Theory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/html/2610.31778"&gt;Benchmarks are Surprisingly Compaction-Friendly&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content></item></channel></rss>