Skip to the solution
MediumAI Engineering17 min read

How do you manage conversations that outgrow the context window?

771 views

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

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.

Solution

The solution is hidden so you can try first

17 min read ยท 13 sections

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).
const BUDGET = 200;                       // window minus reserved output tokens
const SYSTEM = "system: You are a support agent for Acme. Never promise dates you cannot see.";
const TURNS = [
  ["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?"],
];
const FACTS = ["1042", "Berlin", "email"];

const tok = m => m.split(" ").length + 4;
const total = msgs => msgs.reduce((s, m) => s + tok(m), 0);
const summarise = msgs => "summary: earlier the user said: " +
  msgs.filter(m => m.startsWith("user:")).map(m => m.split(" ").slice(1, 5).join(" ")).join(" | ");

function sliding(history, newMsgs) {               // drop oldest messages, keep tool pairs together
  while (total([SYSTEM, ...history]) > BUDGET) history.splice(0, history[0].startsWith("tool_use") ? 2 : 1);
  return [SYSTEM, ...history];
}

let summaryState = [];                    // messages already folded into the summary
function summariseAll(history, newMsgs) {          // on overflow, fold everything but the last 2
  if (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 arrive
let published = [], folded = [];          // the facts block changes only at compaction
function tiered(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]);
  }
  const build = () => [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 first
      if (history[i].startsWith("tool_result")) history[i] = "tool_result: [cleared, re-run tool if needed]";
    published = [...ledger];              // 2) publish the facts, 3) fold old turns
    while (total(build()) > BUDGET * 0.6 && history.length > 4) folded.push(history.shift());
    tiered.compactions++;
  }
  return build();
}
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)));

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 32 of 83 in AI engineering

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextMedium

Hybrid search: why combine BM25 with vectors and how does RRF work?

Continue