How do you manage conversations that outgrow the context window?
771 views
32 of 83
1
The problem
Question presented to candidate:
"Our support assistant keeps the whole chat history in every request. Long sessions now hit the context limit, get slow and expensive, and the bot starts forgetting what the customer said at the start. How would you redesign how conversation history is managed?"
What a strong answer should cover:
Why the naive fixes fail: a sliding window silently drops early facts (and must never drop the system prompt or split a tool call from its result); a single lossy summary keeps the gist and loses the specifics
A tiered design: system prompt, a small facts or state block extracted from the conversation, a rolling summary of older turns, the most recent turns verbatim, and retrieval over the archived full history for anything else
When and how to compact: a high-water mark with headroom for the reply, compacting down to a low-water mark so it does not run every turn, clearing old tool results before summarising, and a structured summary prompt that must keep IDs, decisions and open tasks
Interplay with prompt caching and cost: an append-only history is a growing cached prefix, so every rewrite of earlier context is a cache miss; compaction should be infrequent and batched
Provider options and evaluation: server-side compaction and context editing (Anthropic), compaction and truncation settings in OpenAI's Responses API, and testing that facts survive compaction with replayed long conversations
Clarifying questions expected:
Is this plain chat, or an agent whose history is mostly tool calls and tool results?
Do we need the exact transcript for audit or replay, or only for the model's context?
How long do sessions run, and does the assistant need to remember across sessions as well?
Code / implementation expected: Optional โ a context builder with a token budget, a compaction trigger and a facts block, or a sketch of the request layout.
memory
summarization
context-window
chat
2
Try it yourself
Sketch your approach before you read the solution. That is what the interview tests.
Stuck? Get a hint.
A short nudge toward the approach. It will not give the answer away.
3
Solution
Target Audience: Engineers running a chat assistant or agent whose sessions get long, and who need a history strategy that is cheaper than "send everything" without making the bot forget.
Difficulty: Medium
Verified with: Node 22.22.0 and Python 3.12.3 (the conversation replay below). Provider compaction features were checked against the Anthropic docs and OpenAI documentation summaries cited inline, in October 2026.
How to read this doc: Every idea is explained in plain language first, then tagged with ๐ Interview term: so you also learn the words an interviewer expects. All program output shown was produced by actually running the example; the Verified section quotes it in full.
What a context window is, what the API does on overflow, and the three basic ways to make a long history fit (cut the oldest, keep the system prompt, summarise) are covered in [What i
The solution is hidden so you can try first
17 min read ยท 13 sections
4
Read the code
Sliding window vs summarise-on-overflow vs tiered memory on one long chat
// Three ways to keep a growing chat inside a small token budget, replayed on the same// conversation. Tokens = words + 4 per message. The "summary" is a stub that keeps the// first 4 words of each user message (a stand-in for a short, lossy model summary).constBUDGET = 200; // window minus reserved output tokensconstSYSTEM = "system: You are a support agent for Acme. Never promise dates you cannot see.";
constTURNS = [
["user: Hi my order 1042 has not arrived and I am worried about it", "assistant: Sorry to hear that. Let me check order 1042 for you right away."],
["user: Please contact me by email only, I cannot take phone calls at work", "assistant: Noted, I will use email for any updates on this case."],
["user: Also I moved last week, please ship to Berlin instead of Munich", "assistant: Thanks, I will update the delivery city before anything is resent."],
["user: Can you look up where the parcel is right now", "tool_use: lookup_order 1042",
"tool_result: status in_transit carrier DHL depot Leipzig last_scan 14 Oct 09:12 weight 1.2kg service standard attempts 0 notes none customs none signature not_required",
"assistant: The parcel is at the Leipzig depot and moving again."],
["user: What does in transit mean for the delivery date", "assistant: It usually means delivery within two to three business days."],
["user: Can you check the depot history too", "tool_use: depot_history 1042",
"tool_result: 12 Oct Hamburg sorted 13 Oct Hamburg departed 13 Oct Leipzig arrived 14 Oct Leipzig sorted 14 Oct Leipzig departed next regional hub expected",
"assistant: It left Leipzig this morning heading to the regional hub."],
["user: OK and is there a refund if it is late", "assistant: If it is more than five days late you can claim a shipping refund."],
["user: Fine. Which order are you tracking, which city will it go to and how will you contact me?"],
];
constFACTS = ["1042", "Berlin", "email"];
consttok = m => m.split(" ").length + 4;
consttotal = msgs => msgs.reduce((s, m) => s + tok(m), 0);
constsummarise = msgs => "summary: earlier the user said: " +
msgs.filter(m => m.startsWith("user:")).map(m => m.split(" ").slice(1, 5).join(" ")).join(" | ");
functionsliding(history, newMsgs) { // drop oldest messages, keep tool pairs togetherwhile (total([SYSTEM, ...history]) > BUDGET) history.splice(0, history[0].startsWith("tool_use") ? 2 : 1);
return [SYSTEM, ...history];
}
let summaryState = []; // messages already folded into the summaryfunctionsummariseAll(history, newMsgs) { // on overflow, fold everything but the last 2if (total([SYSTEM, ...summaryState.length ? [summarise(summaryState)] : [], ...history]) > BUDGET) {
summaryState.push(...history.splice(0, history.length - 2));
}
return [SYSTEM, ...(summaryState.length ? [summarise(summaryState)] : []), ...history];
}
const ledger = []; // facts pulled out of user turns as they arrivelet published = [], folded = []; // the facts block changes only at compactionfunctiontiered(history, newMsgs) {
for (const m of newMsgs) {
let r;
if ((r = m.match(/order (\d+)/)) && !ledger.includes("order " + r[1])) ledger.push("order " + r[1]);
if ((r = m.match(/ship to (\w+)/))) ledger.push("ship to " + r[1]);
if ((r = m.match(/contact me by (\w+)/))) ledger.push("contact by " + r[1]);
}
constbuild = () => [SYSTEM, ...(published.length ? ["facts: " + published.join("; ")] : []),
...(folded.length ? [summarise(folded)] : []), ...history];
if (total(build()) > BUDGET * 0.85) { // high-water mark (85%) hit: compact down to 60%for (let i = 0; i < history.length - 4; i++) // 1) clear old tool results firstif (history[i].startsWith("tool_result")) history[i] = "tool_result: [cleared, re-run tool if needed]";
published = [...ledger]; // 2) publish the facts, 3) fold old turnswhile (total(build()) > BUDGET * 0.6 && history.length > 4) folded.push(history.shift());
tiered.compactions++;
}
returnbuild();
}
tiered.compactions = 0;
let finalTiered = [];
for (const [name, fn] of [["sliding window", sliding], ["summarise on overflow", summariseAll], ["tiered memory", tiered]]) {
const history = [];
let prev = [], rewrites = 0, peak = 0, ctx = [];
TURNS.forEach(turn => {
history.push(turn[0]); // user message arrives, then the request is built
ctx = fn(history, [turn[0]]);
if (prev.some((m, i) => ctx[i] !== m)) rewrites++; // earlier context changed: cache prefix broken
peak = Math.max(peak, total(ctx));
prev = ctx;
history.push(...turn.slice(1)); // assistant reply and tool messages
});
const text = ctx.join(" ");
if (fn === tiered) finalTiered = ctx;
const kept = FACTS.filter(f => text.includes(f));
console.log(`${name.padEnd(22)} final ${total(ctx)} tokens, peak ${peak}/${BUDGET}, prefix rewrites ${rewrites}/${TURNS.length}, facts in context ${kept.length}/3 [${kept.join(", ")}]`);
}
console.log(`full history would be ${total([SYSTEM, ...TURNS.flat()])} tokens; tiered compactions: ${tiered.compactions}`);
console.log("\nfinal tiered context sent with the last question:");
finalTiered.forEach(m =>console.log(" " + (m.length > 90 ? m.slice(0, 87) + "..." : m)));
5
Discussion
Other approaches, follow-ups and how it went in real interviews.