Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Spot the Bug in AI Code
An assistant wrote this history-trimmer to fit a chat inside the context window. It looks right, the types check, it passes the obvious case — and it silently drops the system prompt the moment the conversation gets long. Reviewing AI code means catching exactly this: confident, plausible, and wrong on the case that matters. Find the bug and fix it.
The problem
The starter code is an AI-written clip_history(history, max_turns). history[0] is the system message and must always survive; the rest are conversation turns, of which only the most recent max_turns should be kept. The current code returns history[-max_turns:] — which counts the system message as a turn and drops it entirely once the history is longer than the budget. Fix it so the system message is always first, followed by at most the last max_turns non-system messages. Do not mutate the input.
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
The AI-written clip_history below drops the system prompt on long conversations. Find the one-line bug and fix it so the system message always survives and only the last max_turns turns are kept.
Examples and constraints 3 examples
history=[sys, u1, u2], max_turns=5[sys, u1, u2]history=[sys, u1, u2, u3, u4, u5], max_turns=2[sys, u4, u5]history=[sys, u1, u2], max_turns=0[sys]- The system message (
history[0]) is always the first element of the result, regardless ofmax_turns. - Keep at most the last
max_turnsof the non-system messages (history[1:]);max_turns=0keeps none. - If the whole history already fits (
max_turns >= len(history) - 1), return it unchanged. - Do not mutate the input
history— build and return a fresh list.
Hints 4 ideas
- Run it first: try max_turns=2 on a history of 6 — watch the system message vanish. That is the bug.
- The system message must not be counted against the turn budget. Separate it out: system = history[0], rest = history[1:].
- Slice the REST, not the whole history: rest[-max_turns:] keeps the last max_turns turns.
- Guard max_turns == 0 — an empty slice, so the result is just [system].
Approach, complexity & discussion — open after you solve
Start from brute force
The obvious fix is a one-line patch that puts the system message back: [history[0]] + history[-max_turns:]. It looks right because it repairs the reported case: with six messages and max_turns = 2 it returns [sys, u4, u5]. It costs O(max_turns) time and space, the size of the tail slice. It is still wrong, because the tail can contain history[0] as well. When the whole conversation fits, as in the first example with a budget of 5, it returns [sys, sys, u1, u2]. With max_turns = 0, history[-0:] is the entire list, so it returns [sys, sys, u1, u2] again: nothing trimmed, and the instructions sent twice.
The approach
The insight is that the system message is not a turn, so it must never compete for the turn budget. Take it out of the pool before counting, apply the budget to what is left, and put it back at the front. The invariant: whatever the history length, the result is [system] followed by the newest min(max_turns, len(history) - 1) turns in their original order.
The reference does exactly that. system = history[0] pins the instruction message. rest = history[1:] holds every turn in a new list, so the input is never touched. kept = rest[-max_turns:] if max_turns > 0 else [] takes the newest turns; the guard exists because rest[-0:] is the whole list, not an empty one, and because slices clamp, a budget larger than the history keeps everything with no IndexError. return [system] + kept builds a fresh list. The TypeScript version mirrors it with slice(1), slice(-maxTurns) and the same maxTurns > 0 check.
Walk through an example
The reference run on all three examples, with the original one-liner’s output in the last row for contrast.
| step | expression | example 1, max 5 | example 2, max 2 | example 3, max 0 |
|---|---|---|---|---|
| 1 | system = history[0] | sys | sys | sys |
| 2 | rest = history[1:] | [u1, u2] | [u1, u2, u3, u4, u5] | [u1, u2] |
| 3 | max_turns > 0 | True | True | False |
| 4 | kept | rest[-5:] = [u1, u2] | rest[-2:] = [u4, u5] | [] |
| 5 | [system] + kept | [sys, u1, u2] | [sys, u4, u5] | [sys] |
| 6 | buggy history[-max_turns:] | [sys, u1, u2] (right by accident) | [u4, u5] (system lost) | [sys, u1, u2] (nothing trimmed) |
Complexity
Reference: O(n) time and space for n messages, because history[1:] copies every turn before the tail is taken; starting the slice at max(1, len(history) - max_turns) instead copies only the kept turns, O(max_turns). One-line patch: O(max_turns) time and space for its single tail slice, but it duplicates the system message whenever that slice reaches index 0.
Edge cases to test
max_turns = 0:[sys].rest[-0:]is the whole list in Python, andslice(-0)is the whole array in JavaScript, so the explicit guard is required.- No turns yet,
[sys]withmax_turns = 3:[sys].restis empty, and so iskept. - A budget that exactly fits,
[sys, a, b]withmax_turns = 2: returned unchanged. The original one-liner already fails here and returns[a, b], because three messages exceed a budget of two. - A budget larger than the history,
[sys, u1, u2]withmax_turns = 5: returned unchanged. Slices clamp to the list in both languages, so no length check is needed. - Editing a returned message: the list is new, but the message dicts are shared with the input, so setting
result[0]['content']also changeshistory[0]. Copy the dicts if callers may modify them.
Common mistakes
- Slicing the whole
historyinstead ofhistory[1:], so the system message competes for the turn budget. - Forgetting the
max_turns == 0case —x[-0:]is the whole list, not empty, a classic Python trap. - Mutating the input list instead of building a fresh one.
- Fixing the symptom (“keep it under N”) without asking whether the system prompt must always survive — it must.
- Patching with
[history[0]] + history[-max_turns:]: once the whole history fits, the tail already includeshistory[0], so the system message is sent twice.
Follow-ups an interviewer asks next
- The budget is tokens, not turns.
- Reserve the system prompt’s tokens first, then walk the turns from newest to oldest, adding each message’s token count, and stop before the first one that would exceed what is left; keep that suffix in original order. Never split a message, and keep an assistant tool call together with its tool result, since chat APIs reject a tool result whose call was trimmed away.
- Trimming drops facts the user stated early in the chat.
- Summarize instead of discarding. When turns fall out of the window, fold them into a running summary message placed right after the system prompt, and update it as more turns age out. The system message stays pinned, the summary carries the old context in far fewer tokens, and the recent turns stay verbatim.
- Several messages must survive, not just
history[0]. - Generalize the split: partition the messages into pinned ones (the system prompt, a summary, a document the user attached) and trimmable ones, apply the budget only to the trimmable list, then merge both back in original order. The invariant is unchanged: nothing pinned ever counts against the budget.
Where this shows up
Dropping the system prompt on long conversations is a real, shipped bug pattern in AI apps — the assistant silently “forgets” its instructions mid-chat. More broadly this is what reviewing AI-generated code is: the code looks right, passes the obvious case, and fails on the case that matters. Catching it means reading for the edge (long history) the demo never exercised — the core skill of shipping with AI.
Reference solution
Reference solution — Python
def clip_history(history, max_turns):
system = history[0]
rest = history[1:]
kept = rest[-max_turns:] if max_turns > 0 else []
return [system] + kept
sys = {"role": "system", "content": "S"}
long_hist = [sys] + [{"role": "user", "content": str(i)} for i in range(5)]
print(clip_history(long_hist, 2))
# -> [system, {'2'}, ... ] keeps system + last 2 turnsReference solution — TypeScript
type Msg = { role: string; content: string };
function clipHistory(history: Msg[], maxTurns: number): Msg[] {
const system = history[0];
const rest = history.slice(1);
const kept = maxTurns > 0 ? rest.slice(-maxTurns) : [];
return [system, ...kept];
}
const sys = { role: 'system', content: 'S' };
const longHist = [sys, ...[0, 1, 2, 3, 4].map((i) => ({ role: 'user', content: String(i) }))];
console.log(clipHistory(longHist, 2));
// -> [system, {content:'3'}, {content:'4'}]Questions about Spot the Bug in AI Code
Why does a Python slice like x[-0:] return the whole list?
Because -0 is just 0, so x[-0:] means x[0:], a slice from the start to the end. A negative index counts from the end only when it is actually negative, and zero has no sign. That is why the reference guards the case explicitly: kept = rest[-max_turns:] if max_turns is greater than 0, else an empty list. JavaScript has the same trap, since array.slice(-0) also returns a copy of the whole array, which is why the TypeScript version has the same check. Without the guard, a budget of zero turns keeps every turn.
How do you keep the system prompt when trimming chat history?
Take it out before you count. Split the history into the system message, history[0], and the turns, history[1:], apply the turn budget only to the turns, and put the system message back at the front. The bug in this challenge, history[-max_turns:], treats the system message as one of the turns, so once the conversation is longer than the budget the slice starts after it and the assistant silently loses its instructions. Pinning the system message outside the budget makes its survival unconditional instead of dependent on how long the chat has run.
Why not just write [history[0]] + history[-max_turns:]?
Because the second slice can still contain history[0]. When the whole conversation fits, as with [sys, u1, u2] and a budget of 5, history[-5:] is the entire list, so the result is [sys, sys, u1, u2] and the model receives its instructions twice. With a budget of 0, history[-0:] is the entire list as well, so nothing is trimmed and the system message is duplicated again. Slicing history[1:] first removes the system message from the pool, so neither failure can happen.
Should chat history be trimmed by turns or by tokens?
By tokens, when the goal is to fit a context window, because turns vary enormously in length: one pasted log can outweigh fifty short replies. A turn budget, as in this challenge, is a simpler proxy that is easy to reason about and to test. A token-based trimmer reserves the system prompt’s tokens up front, then walks the turns from newest to oldest, adds each one’s token count, and stops before the budget would be exceeded. Either way, the system prompt stays pinned outside the budget.
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- Two SumThe classic warm-up: find the two numbers that add up to a target. Brute force is O(n²) — a hash map gets you to one pass, O(n). Solve it in Python or TypeScript, right in your browser, with hidden tests and a reveal-solution button.Read →
- Valid ParenthesesThe canonical stack problem: decide whether every bracket is closed by the right type, in the right order. A stack turns nested matching into a single pass. Solve it in Python or TypeScript with hidden tests.Read →
- Fizz BuzzThe famous screening question. Print 1…n, but multiples of 3 become "Fizz", of 5 become "Buzz", and of both become "FizzBuzz". Easy — the catch is testing divisibility in the right order. Solve it in Python or TypeScript.Read →
- SoftmaxThe function at the end of every classifier and language model: turn raw scores (logits) into a probability distribution. Implement the numerically stable version so big logits do not overflow. Solve it in Python or TypeScript.Read →
- Cosine SimilarityThe measure behind every embedding search and RAG system: how aligned are two vectors, ignoring their length? Dot product over the product of magnitudes — 1 identical, 0 orthogonal, -1 opposite. Solve it in Python or TypeScript.Read →
- Top-K RetrievalThe core of the "R" in RAG: given a query embedding and a set of document embeddings, return the indices of the k most similar docs by cosine similarity, with a stable tie-break. Solve it in Python or TypeScript.Read →