Module 8 of 30
Conversation
Management

The API has zero memory. Every "conversation" you've ever had with Claude is your code re-sending the entire transcript on every turn. Five concepts, from statelessness to a production-grade conversation manager.

Track 3: Memory & Context ⏱ ~15 min read 8 / 30
Concept 1 of 5

Claude has no memory

The Claude Messages API is stateless. There is no session, no server-side storage, no implicit memory of any kind. Every API request is processed in complete isolation from every other request your code has ever made.

What feels like a "conversation" is an illusion built by your application. On every single turn, your code replays the entire transcript — user message 1, assistant reply 1, user message 2, assistant reply 2, and so on — in the messages array. If you leave a turn out, it never happened as far as Claude is concerned.

Concept 1 of 5

The amnesiac expert

BEFORE: Picture a world-class consultant with perfect amnesia. Brilliant, fast, articulate — but every time you walk into the room, they have zero memory of any previous meeting.

PAIN: You walk in and say "so what do you think about option B?" They stare blankly. They don't know what option A was, who you are, or what problem you are solving. Every meeting starts from absolute zero.

MAPPING: Each Claude API call is a fresh consultant in a fresh room. The only way to give them "memory" is to hand them a written transcript on the way in. Your messages array is that transcript — and you, the developer, are the one carrying it from meeting to meeting.

Concept 1 of 5

Three turns, three full transcripts

  1. Turn 1Send user 1. Claude responds. Your code stores both in a list. Server forgets the call the moment the response leaves.
  2. Turn 2Send user 1, asst 1, user 2 — all of it, every byte. Claude responds. Append the reply.
  3. Turn NSend everything that came before plus the new turn. Token usage grows linearly with the turn count.
  4. Pop on failureIf a call errors, pop the last user message you appended — otherwise the next call sends two user turns in a row, which the API rejects.

The "memory" you experience in claude.ai is built the exact same way: the client replays the whole transcript on every request.

Concept 1 of 5

Where does memory live?

# Question: I want my agent to "remember" X. Where do I put it?

IF X must persist across the SAME conversation:
  PUT X in the messages array
  # this is the model's only working memory

ELIF X must survive a server restart or new device:
  SAVE messages array to disk / DB / Redis
  LOAD on next request & resend
  # the array IS the conversation

ELIF X is a long-lived user preference / fact:
  STORE in your app DB
  INJECT into system prompt every request
  # cheaper than carrying it in history forever

ELSE (single-call, no follow-up):
  JUST CALL Claude, no state needed

There is no fourth option. The model itself can't remember anything. Every retention strategy is a way of putting bytes back into the next request.

Concept 1 of 5

Misconceptions

"Claude remembers our previous conversation."
No. The Messages API has no server-side storage. If your messages array doesn't include a turn, it never happened. Every "memory" feature in production is your code replaying past turns.
"Statelessness is a limitation."
It's a superpower. Because you control the entire context, you can edit history, branch conversations, redact PHI before logging, or rewind to turn 5 and try a different path. Server-managed sessions can't do any of that.

The Messages API is stateless. Memory is your application's job. The messages array IS the model's memory — nothing else persists between calls.

Build conversations by saving messages on your side and resending them. Every other concept in this module is a smarter way of doing that one thing.

Concept 2 of 5

Three ways to ship the past

Once you accept that you must resend history, the real question is which history. There are three canonical patterns. Full history sends every turn ever — simplest, but tokens grow forever. Sliding window keeps only the last N turns — fixed cost, but silently drops early facts. Summarization compresses old turns into a short paragraph and keeps recent turns verbatim — the production sweet spot.

Real systems usually combine all three: full history for the last 4 turns, a rolling summary for everything older, and "pinned" facts that never get summarized. This hybrid is what every serious customer-support, healthcare, and coding agent uses under the hood.

Concept 2 of 5

Packing for a two-week trip

BEFORE: Imagine packing for two weeks abroad with one carry-on at a strict 7 kg limit. No checked luggage allowed.

PAIN: Bring everything (full history) and the bag overflows at the gate. Bring only what fits today (sliding window) and you arrive at a formal dinner in hiking clothes because you tossed the dress shoes on day three.

MAPPING: The carry-on is your context window. Summarization is the trick the seasoned traveler uses: pack the last few days in full, plus a short note — "left two suits at home, bringing the navy one" — that captures everything else without the bulk. The trip survives. Your context survives.

Concept 2 of 5

Three strategies, side by side

After 20 turns, sending turn 21:

  1. Full historySend all 20 prior messages plus the new one. ~3,000 tokens. Trivial code, breaks past ~100 turns.
  2. Sliding window (N=6)Send only the last 6 + new. ~1,050 tokens. Fixed cost forever — but turn 1 facts vanish at turn 7.
  3. SummarizationSend a 1-paragraph summary of turns 1–14, then turns 15–20 verbatim, then the new one. ~1,400 tokens. Bounded cost, key facts preserved.
  4. Hybrid (production)Pinned facts at top (account #, preferences) · rolling summary · last 4–6 turns full. The shape every serious agent ends up with.

Cost reality: a 50-turn support chat is ~$0.23/reply on full history vs ~$0.03/reply on summarization. At 10,000 chats/day that is $2,000/day saved.

Concept 2 of 5

Pseudocode

# A. FULL HISTORY  -- send everything, every time
FUNCTION full_history(history, new_msg):
  RETURN history + [new_msg]

# B. SLIDING WINDOW  -- keep only the last N turns
FUNCTION sliding(history, new_msg, n=6):
  trimmed = history[-n:]
  RETURN trimmed + [new_msg]

# C. SUMMARIZE  -- compress old, keep recent
FUNCTION summarize_strategy(history, new_msg, keep=4):
  old = history[:-keep]
  recent = history[-keep:]

  IF len(old) > 0:
    summary = CALL Claude("summarize, keep IDs/names verbatim", old)
    RETURN [
      {role: "user", content: "[SUMMARY] " + summary},
      {role: "assistant", content: "Got it."}
    ] + recent + [new_msg]

  RETURN recent + [new_msg]

Always end with a real user message and keep the user/assistant alternation valid — the API rejects two user turns in a row.

Concept 2 of 5

Misconceptions

"Sliding window is good enough for production."
Dangerous. If the user gave their account number in turn 1 and your window is 10, that fact vanishes at turn 11 — and the bot starts asking for it again. Safe only when no early turn carries critical state.
"Summarization is lossless compression."
It isn't. Summaries paraphrase, and details — exact IDs, dollar amounts, dates, dosages — quietly disappear. That is why production systems pin critical facts in a never-summarized block.

Three patterns, one truth: full history, sliding window, and summarization all just decide which subset of past turns ships in the next request.

Default to summarization with a small recent window. It's the only one that scales without losing information — especially when paired with pinned facts.

Concept 3 of 5

One window, four tenants

Every Claude request has a fixed total budget — for example, 200,000 tokens. Four things compete for that space: the system prompt, the conversation history, the current user message, and the reserved response (the max_tokens you set). Together they cannot exceed the window.

The math is simple but unforgiving: available = window - system - history - new_msg - max_tokens. Cross zero and the API rejects the request. Worse, even before that limit hits, recall starts degrading because of "lost in the middle" — facts buried in a 180K context get less attention than facts at the start or end.

Concept 3 of 5

The fixed-size suitcase

BEFORE: You have a single suitcase, exactly 200,000 cm³ — not one cm more — for an international trip. Everything you need must fit, and you must leave room for souvenirs to bring home.

PAIN: Stuff it with the photo album of every prior trip (uncapped history) and there's no room for today's clothes (current message), let alone the souvenir space (response). At the airport, the bag fails the size check and you get rebooked. In API terms, the request errors and the user gets a spinning circle.

MAPPING: The suitcase is the context window. The travel guide is the system prompt (always packed). The album is conversation history (grows every trip). Today's clothes are the new message. The empty space is max_tokens. Pack like an engineer, not a pack rat.

Concept 3 of 5

Worked example, turn 25

  1. Total window200,000 tokens. The hard ceiling for a single request.
  2. System prompt1,200 tokens, fixed. Sent on every call — cumulative billing across the whole chat.
  3. History (25 turns)~37,500 tokens. Grows linearly — every turn adds another user+assistant pair.
  4. New user message~800 tokens. Variable per turn.
  5. max_tokens (reserved reply)4,096 tokens. You set this; the model never returns more than this.
  6. Remaining slack200,000 − 1,200 − 37,500 − 800 − 4,096 = 156,404 tokens. Plenty — today.

By turn 120 the same chat has ~180K of history alone. Slack drops to ~14K and the next reply may truncate. Trim BEFORE the budget runs out, not after.

Concept 3 of 5

Pseudocode

# Compute remaining slack before every API call
FUNCTION available_for_history(window, sys, msgs, new_msg, max_tok):
  used = sys + tokens(msgs) + tokens(new_msg) + max_tok
  RETURN window - used

# Pre-flight check: trim BEFORE you call
FUNCTION guarded_send(state, new_msg):
  slack = available_for_history(
    window=200_000,
    sys=tokens(state.system),
    msgs=state.messages,
    new_msg=new_msg,
    max_tok=4096
  )

  IF slack < 10_000:
    # budget is tight -- compress before sending
    state.messages = summarize_strategy(state.messages)

  reply = CALL Claude(state.system, state.messages + [new_msg])
  # real-time signal: reply.usage.input_tokens
  state.last_input_tokens = reply.usage.input_tokens
  RETURN reply

Treat response.usage.input_tokens as your live budget gauge. When it climbs, trim on the next turn — not after the API returns a 400.

Concept 3 of 5

Misconceptions

"Context window equals memory."
No. The window is the temporary working space for ONE request. It evaporates the moment the response returns. Persistent memory lives in your application — the window is just how much of that memory you can ship per call.
"More tokens always means better answers."
Recall degrades with very long contexts — the "lost in the middle" effect. Stuffing 180K of history in often performs worse than a tight 20K with the right facts on top. Pruning improves accuracy and saves money simultaneously.

Four tenants share one window: system + history + current message + reserved max_tokens. Add them up before every call.

Trim early, not late. A trim BEFORE the call costs one extra summary; a trim AFTER a 400 error costs you a failed user request and a debugging session.

Concept 4 of 5

Cut the noise, keep the signal

Pruning is the art of removing messages without losing information density. Not all messages are equal — "thanks!" carries no context value, while "my account is #4521" must survive every trim. Smart pruning scores each message by importance and drops the cheap ones first.

Five strategies stack on top of each other: FIFO (drop oldest), importance scoring (drop low-value first), semantic dedup (drop repeated questions), role-based retention (always keep tool results & key decisions), and summarize-and-replace (compress a block before dropping). Production systems combine three or four of them in a single pipeline.

Concept 4 of 5

The film editor

BEFORE: You have 6 hours of raw footage that must become a 90-minute movie. Key plot points are scattered throughout — some in the first 10 minutes, some in the final 20.

PAIN: Cut blindly from the start and you lose the character introduction that makes the climax meaningful. Cut blindly from the end and you spoil the ending. Keep everything and the audience checks out during filler scenes.

MAPPING: Pruning a conversation works the same way. Greetings and "thanks" are filler — cut first. Account numbers and tool results are plot points — preserve at all costs. Sometimes you replace a block of scenes with a short "previously on this conversation..." narration. Importance score is the editor's gut.

Concept 4 of 5

The five-strategy stack

  1. FIFODrop the oldest user/assistant pair first. Simplest. Risky if turn 1 carries critical state.
  2. Importance scoringTag each message with high / medium / low. Drop low first, then medium, never high. ~60% token cut while keeping 90%+ of signal.
  3. Semantic dedupIf the user asked the same thing twice, drop the duplicate exchange. Cheap, surprisingly effective on long support chats.
  4. Role-based retentionAlways keep tool results and explicit user decisions, regardless of age. These often contain numbers Claude can't recompute.
  5. Summarize-and-replaceTake a block of low/medium messages, send to Claude with "summarize, keep IDs verbatim", replace the block with the summary. Pair with strategy 2 for best results.

After every prune, verify user/assistant alternation is intact — remove a user message and the orphaned assistant reply must go too, or the next API call fails.

Concept 4 of 5

Pseudocode

# Each message carries metadata
SCHEMA Message:
  role: "user" | "assistant"
  content: text
  importance: "high" | "medium" | "low"
  tags: ["account_id", "tool_result", ...]

FUNCTION prune(messages, target_tokens):
  # 1. drop low-importance pairs (oldest first)
  WHILE tokens(messages) > target_tokens:
    pair = oldest_pair_with(messages, importance="low")
    IF pair: messages.remove(pair); CONTINUE

  # 2. summarize medium-importance block if still over
    medium = oldest_block_with(messages, importance="medium")
    IF medium:
      summary = CALL Claude("summarize, keep tags verbatim", medium)
      messages = replace(messages, medium, [summary_pair(summary)])
      CONTINUE

  # 3. NEVER drop "high" -- raise instead
    RAISE ContextOverflow("can't trim further")

  assert valid_alternation(messages)
  RETURN messages

Importance is set on insert. The cheapest signals work: tag tool results "high", greetings "low", everything else "medium".

Concept 4 of 5

Misconceptions

"Pruning only matters for very long conversations."
Even a 15-turn chat with tool calls can hit 50K+ tokens. Tool results — JSON payloads, DB rows, API responses — are often larger than human messages. A single tool result can be 2,000–5,000 tokens.
"Any message can be pruned safely."
No. If turn 12 says "Based on order #889..." and you remove the turn that introduced #889, the reference becomes a non sequitur. Always prune in user/assistant pairs and check for downstream references.

Pruning is information density preservation. Drop noise (greetings, dedup'd questions), summarize medium blocks, and never touch tagged high-importance facts.

Real-world impact: a 20-message support chat goes from ~3,000 tokens to ~1,200 (60% reduction) while retaining ~91% of the actionable information.

Concept 5 of 5

One class to rule the chat

A production conversation manager is a single class that sits between your application and the Claude API. It owns the messages array, tracks tokens, triggers pruning, summarizes when needed, attaches metadata, and serializes state to disk so conversations survive a server restart.

Without one, every endpoint in your codebase rolls its own message-handling logic — and every endpoint hits the same bugs: phantom user turns, broken alternation, runaway token costs, lost account numbers. With one, you write the logic once and every feature inherits it.

Concept 5 of 5

The chief of staff

BEFORE: Imagine a CEO who takes every meeting personally, walking in with a growing stack of every note from every past meeting — verbatim, unfiltered, in chronological order.

PAIN: By month three, the stack is so tall the CEO spends more time flipping through old notes than engaging in the current meeting. Decisions from week one are buried under pages of "sounds good" and "let's circle back". Sometimes the CEO misses critical commitments entirely.

MAPPING: A conversation manager is that CEO's chief of staff. It records every exchange, scores messages by importance, compresses old meetings into executive summaries, pins critical decisions to the top, and ensures the CEO walks into every meeting with exactly the right briefing — recent details in full, older context summarized, key facts always on top.

Concept 5 of 5

Six responsibilities

  1. Message storageAppend, retrieve, and edit messages in a single ordered array. The source of truth.
  2. Token countingTrack per-message and cumulative input/output tokens. Always know how close to the budget you are.
  3. Automatic pruningTrigger when tokens cross a configurable threshold (e.g., 100K). No manual babysitting from app code.
  4. SummarizationUse Claude itself to compress old turns into a short paragraph — with explicit instructions to keep IDs and names verbatim.
  5. Metadata trackingTimestamps, token counts, importance flags, tool-result tags. The fuel for smart pruning.
  6. SerializationSave and load full state to JSON. Conversations survive server restarts, deploys, and user device switches.

Real impact: a healthcare pre-auth agent at 500 cases/day drops from $34/day to $12/day on input tokens (65% cut) when wrapped by a tuned manager — while keeping 95%+ accuracy on clinical references.

Concept 5 of 5

Pseudocode

CLASS SmartConversationManager:
  system_prompt
  messages = []
  token_threshold = 100_000
  recent_to_keep  = 4
  pinned_facts    = []   # never summarized

  METHOD send(user_text, importance="medium"):
    self.messages.append({user, user_text, importance})

    IF tokens(self.messages) > self.token_threshold:
      self.compress()

    TRY:
      ctx = self.pinned_facts + self.messages
      reply = CALL Claude(self.system_prompt, ctx)
      self.messages.append({assistant, reply})
      RETURN reply
    CATCH error:
      self.messages.pop()         # keep alternation valid
      RAISE

  METHOD compress():
    old   = self.messages[:-self.recent_to_keep]
    keep  = self.messages[-self.recent_to_keep:]
    summary = CALL Claude("summarize, keep IDs verbatim", old)
    self.messages = [
      {user, "[SUMMARY] " + summary, importance="medium"},
      {assistant, "Got it."}
    ] + keep

  METHOD save(path):  write_json(path, self.__dict__)
  METHOD load(path):  self.__dict__ = read_json(path)

The full desktop module ships three working classes — basic, sliding window, and smart — in Python and Node.js, with token counting and JSON persistence built in.

Concept 5 of 5

Misconceptions

"I'll just inline the messages array in my route handler."
Works for one endpoint. Falls apart at three. Pruning, summarization, token counting, error recovery, and persistence all need to be solved — do it once in a class, not five times in five route handlers.
"Pin everything important — it's safer."
Pin too much and your "pinned facts" block grows to 30K tokens, which is just history with extra steps. Pin only items that are structurally critical: account numbers, case IDs, explicit user preferences, regulated facts. Cap the pinned block at ~2K tokens.

The conversation manager is the unsung hero of every production agent. One class encapsulates statelessness, history pattern, token budget, pruning, and persistence.

Cert tip (Domain 5.1): position immutable "case facts" at the START of context — the high-recall position — and never summarize them. Names, IDs, amounts, and dates belong there.

One question per concept

Tap a card to reveal the answer.

Open the full module on desktop

The desktop version walks through three working ConversationManager classes — full history, sliding window, smart-with-summarization — with token-budget math, error recovery, persistence to JSON, and benchmark comparisons in Python and Node.js.

Open M08 on Desktop → Full code · Hands-on lab · ~55 min

Module 8 of 30 · Track 3: Memory & Context