Module 3: Memory: The Agent That Remembers
7. When to forget: summarizing and resetting the context
Description
By the end of this lesson you'll be able to build, inside an n8n workflow, the mechanism that compacts a conversation's history before it falls out of the context window — without losing the data that matters — and you'll be able to decide, with a concrete criterion and not by intuition, when it's worth summarizing that history and when it's worth resetting it completely instead of dragging it along.
This matters for any agent that holds genuinely long conversations: a technical support ticket that stretches over several days, a sales negotiation that goes back and forth over WhatsApp, an HR assistant that accompanies an entire onboarding process. In those cases it isn't a remote possibility that the conversation grows beyond what fits in the window — it's the expected outcome. And the naive solution of raising contextWindowLength to a huge number solves nothing: you already saw in the previous lesson that more accumulated raw history isn't free, it degrades the model's own precision. The question isn't "how big do I make the window," it's "what do I do with what falls out of it."
Connection to the module: in lesson 6 you saw context drift's symptoms and the general mitigation strategies, in broad strokes. This lesson takes one of those strategies — summarizing — and builds it start to finish inside n8n: which nodes to use, in what order, with what prompt, and with what criterion to decide whether it's better to summarize or to throw out the complete history and start from zero. It doesn't re-explain why drift happens — you already saw that — and it doesn't yet build the complete agent with persistent per-user memory either; that's lesson 8's mini-project, which is going to use exactly what you build here as one more piece of the final agent.
Compacting the history: summarizing before the window discards it
Think of someone taking notes at a meeting that's already run two hours and is going to run longer. Transcribing word for word is impossible — and wouldn't be very useful either — so every so often that person pauses, rereads the last thing they wrote in detail, and condenses it into three or four lines: who said what, what got decided, what's still pending. Then they keep taking notes for the rest of the meeting with the same level of detail as always. The result isn't a complete transcript or a one-line summary — it's a mix: the old part, condensed; the recent part, intact.
Anthropic names this technique compaction in its context engineering guide for agents, and defines it this way: "Compaction is the practice of taking a conversation nearing the context window limit, summarizing its contents, and reinitiating a new context window with the summary."
You already know, from lesson 3, that contextWindowLength doesn't delete anything on its own in a persistent backend like Postgres Chat Memory: the table keeps saving every turn forever. What contextWindowLength decides is how many of the most recent turns get reconstructed in the message array the model receives on every call. Every turn older than that window is still, technically, there, in the database — but it stops being something the model can see, which for practical purposes is the same as it never having existed. Compacting is the technique that avoids that silent loss: before a block of turns falls out of the window, you condense it into a single message — usually of type System — that does make it into the window, and that carries inside the data that mattered from those turns.
Worked example
You're going to build this on top of a HostNimbus (hosting and domains) technical support agent, with Postgres Chat Memory connected to ai_memory, sessionKey = {{ $json.ticketId }}, and contextWindowLength = 10. Ticket #4821 is a domain migration (hostnimbus-demo.com) that's already at 16 turns of troubleshooting when you decide to compact.
Step 1 — the trigger. After every agent response, a Chat Memory Manager node (Get Many Messages operation, with Simplify Output turned on) measures how many turns are saved for that sessionKey. An IF compares that count against a threshold with margin — not the same number as contextWindowLength, but a higher one, so it doesn't compact on every turn:
# Chat Memory Manager — Operation: Get Many Messages
# Simplify Output: true
memory.sessionKey = "{{ $json.ticketId }}" # 4821
# IF — only triggers the compaction branch with plenty of margin
condition: {{ $json.messages.length }} >= 16 # well above contextWindowLength = 10
Step 2 — separate old from recent. With a Code node you split the array: the first 10 turns are going to be summarized; the last 6 are kept intact, so the agent doesn't lose precision on the most recent part of the conversation.
// Code node — splits the saved history into two parts:
// what's going to be summarized and what stays intact
const messages = $input.first().json.messages;
const keepRaw = 6; // last N turns that are preserved as-is
return [
{
json: {
oldMessages: messages.slice(0, messages.length - keepRaw),
recentMessages: messages.slice(messages.length - keepRaw),
},
},
];
Step 3 — the summary. A Basic LLM Chain node receives oldMessages and produces a compact paragraph, with a prompt that explicitly states what to preserve and forbids making things up:
# Basic LLM Chain — Prompt
Summarize the following technical support conversation in a single
paragraph. Explicitly preserve: 1) any identifier the customer gave
(domain, ticket number, account); 2) the steps already tried and their
result; 3) the problem's current status and what's still pending. Do
not add any data that isn't explicit in the text below — if something
wasn't said, don't make it up.
Conversation:
{{ $json.oldMessages }}
Step 4 — replace. Two calls to the Chat Memory Manager, in order. First, Insert Messages with the mode set to Override All Messages (not Insert Messages), type System, and the summary as the content — this deletes the 16 saved turns and leaves a single message. Then, a second Chat Memory Manager on Insert Messages with no override, which reinserts the 6 turns from recentMessages exactly as they were, preserving their original type (User or AI), so they land after the summary.
# Chat Memory Manager #1 — Operation: Insert Messages
# Mode: Override All Messages
type: "System"
message: "{{ $json.summaryText }}"
# Chat Memory Manager #2 — Operation: Insert Messages
# Mode: Insert Messages (adds, doesn't replace)
# — one per turn in recentMessages, with its original type (User / AI)
What to expect. Before this routine, the Postgres table for ticket #4821 had 16 saved turns. Afterward, it has 7 messages: 1 summary + the 6 most recent turns intact. The next time the customer writes in, the array Postgres Chat Memory builds for the model looks like this:
messages = [
{ role: "system", content: "You are the technical support assistant
for HostNimbus..." }, # agent's fixed System Message
{ role: "system", content: "Summary of the conversation through turn
10: the customer (ticket #4821) migrated
the domain hostnimbus-demo.com 2 days ago.
They already changed the NS to
ns1/ns2.hostnimbus.com. Propagation
completed, but the 'app.' subdomain still
returns NXDOMAIN. Browser cache and the
customer's local DNS were ruled out.
Pending: check whether the subdomain's A
record exists in the zone panel." },
{ role: "user", content: "..." }, # turn 11, preserved as-is
{ role: "assistant", content: "..." },
# ... turns 12 to 16, intact ...
{ role: "user", content: "did you already check what I told you
at the start about when I changed the
NS?" } # turn 23, the new message
]
The "2 days ago" data isn't in any raw turn from the last 10 — it came from turn 3, which already got compacted. But it's still available, because it got written into the summary. The agent can respond: "Yes, you changed the NS 2 days ago, on July 19. Let's check whether the subdomain's A record exists in your zone panel." Without compaction, that data simply wouldn't be in any message the model could read — the same "forgetting" symptom you saw in lesson 2, but caused this time by the window, not by a lack of connected memory.
One detail worth anticipating: this routine fires again later in the same conversation, when the turn count crosses the threshold again. In that second pass, the summary that already exists goes in as part of oldMessages — it gets summarized together with the new turns that aged out — and an updated summary comes out that replaces the previous one. That works fine once or twice. But every summarization pass is, again, a re-reading done by a model — and that's where this lesson's second half comes in: there's a point where continuing to summarize stops being the right call.
Resetting the context: the fresh page
Stick with the person taking meeting notes. If at hour three the group completely switches topics — from budget to a staffing issue that has nothing to do with it — the right move isn't to summarize the budget into one line and keep going on the same page: the right move is to close that page, file it, and start a fresh page for the new topic. Forcing everything onto the same page doesn't save space — it mixes two conversations that shouldn't have been mixed, and makes it harder to separate one from the other later, instead of easier.
Resetting the context is exactly that: stop reading the accumulated history and start the next interaction with nothing prior, instead of compressing it. It's not always the same as "deleting the data" — that distinction matters, and it comes back in this lesson's common mistakes.
Four signals for deciding
| Signal | Summarize and continue? | Reset? |
|---|---|---|
| Same case, the conversation just got longer | Yes | No |
| The case closed (ticket resolved, sale closed) and a new topic shows up | No | Yes |
| There have already been two or more rounds of "summary of the summary" on the same thread | No — every additional round risks more drift | Yes, or at least freeze the current summary as an external reference and start light |
| The customer's intent changes completely (from a technical problem to a billing one, from a purchase to an unrelated complaint) | No | Yes |
Go back to the example. Two weeks later, ticket #4821 closes: the A record got fixed and the subdomain now resolves. Three days after that, the same customer writes through the same channel asking about a duplicate charge on their invoice — a topic that has nothing to do with DNS.
Reset mechanics in n8n
Option A — the reset happens on its own, by scope design. If, as in this case, sessionKey = {{ $json.ticketId }} (lesson 4's scope decision), a new ticket automatically carries a different ticketId, and with it, a different sessionKey. The billing inquiry opens ticket #4901, not #4821, so Postgres Chat Memory starts with nothing saved under that new key. You didn't compact or delete anything by hand — the reset came for free, as a consequence of having chosen scope well back in lesson 4.
Option B — an explicit reset within the same session. Sometimes you can't redesign the scope — the sessionKey is still, say, the customer's phone number for everything they discuss with the company — and you need to clear the history by hand when you detect a case closing or a topic change. That's what the Chat Memory Manager's other operation is for:
# Chat Memory Manager — Operation: Delete Messages
# Mode: All Messages (not Last N)
memory.sessionKey = "{{ $json.phone }}"
What to expect. After this operation, the Postgres table no longer has any rows under that sessionKey. The customer's next message starts with no previous turns in the array Postgres Chat Memory builds — exactly like Configuration A from lesson 1: no history, no compaction dragged along.
Common mistakes
Assuming a summary preserves everything that mattered, without verifying it (conceptual). What happens: the compaction routine gets connected, tested once, it works, and from then on the summary gets trusted blindly — until weeks later the agent responds poorly because the summary left out a piece of data that did matter (a deadline, an exception that was agreed on verbally). Why it happens: summarizing isn't a mechanical operation like truncating text — it's a task performed by a model, with its own margin of error. An LLM can misjudge what's "important" in a long conversation, especially if the summary prompt is vague. How to spot it: check the generated summary against the original conversation, at least on the first few runs, specifically looking for concrete data (numbers, dates, identifiers) that got lost or distorted. How to fix it: write the summary prompt with an explicit list of what to preserve — like this lesson's: identifiers, steps already tried, pending status — and an explicit instruction not to make things up; a generic prompt like "summarize this conversation" leaves too much judgment loose in the model's hands.
Confusing changing the sessionKey with deleting the history (conceptual). What happens: someone says "to reset the customer's memory I changed their sessionKey, that record doesn't exist anymore" — and assumes the previous data disappeared. Why it happens: changing the sessionKey does achieve the effect that matters to the agent (that it doesn't read that history), and that's easily confused with "deleting it." But in Postgres Chat Memory the previous sessionKey's rows keep existing in the table exactly as they were — they're still queryable if someone looks them up with the old key. How to spot it: ask yourself whether the real need is "the agent should stop seeing this" (scope) or "this should stop existing in the database" (storage) — it's the same distinction from lesson 1 of this module. How to fix it: if you genuinely need the history to stop existing — for example, because of a data deletion request — use Delete Messages with the mode set to All Messages on that specific sessionKey; changing the key doesn't meet that requirement.
Triggering compaction at the wrong moment — too late or on every turn (practical). What happens: the trigger threshold is set equal to contextWindowLength, or no threshold is set at all and the routine runs on every turn. In the first case, by the time it's detected that compaction is needed, there's already been at least one turn where the model responded without seeing something that had fallen out of the window — you reacted one step late. In the second case, extra calls to the model get spent constantly summarizing, and every additional "summary of the summary" pass accumulates more risk of losing precision, unnecessarily. Why it happens: it's not obvious, the first time you build this routine, that the trigger threshold needs to leave margin relative to the window's real limit. How to spot it: if the agent loses data that was right at the edge of the last turns, check whether the trigger threshold is equal to or lower than contextWindowLength; if the workflow's token cost rises noticeably, check whether compaction runs more often than needed. How to fix it: set the threshold well above contextWindowLength — in this lesson's example, 16 against a window of 10 — and keep a block of recent turns untouched after every compaction, like you did with recentMessages, so a good stretch of conversation passes before it's needed again.
Exercises
Exercise 1 — Summarize or reset. A car dealership's agent has been going 40 turns with the same customer, comparing financing plans for the same car — the customer still hasn't decided. Summarize and continue, or reset? Two months later, with the car already bought, the same customer (same sessionKey, based on their phone) writes asking about a warranty claim. Summarize and continue, or reset? Justify each case with this lesson's signals.
See solution
Case 1: summarize and continue. It's still the same case — the same financing negotiation — and it just got longer; the topic didn't change and it didn't close. Case 2: reset. The previous case (buying the car) already closed with the sale, and the warranty claim is a new topic with no relation to the financing negotiation. Dragging that summary along doesn't help resolve the warranty claim, and it can confuse the agent by anchoring it to irrelevant data — for example, the negotiated price, when what matters now is the mechanical problem.
Why it works: the criterion isn't "how much time passed" or "how many turns have accumulated" — it's whether it's still the same case or whether the previous case closed and a different one showed up.
Exercise 2 — Design the threshold. In this lesson's example, contextWindowLength = 10 but the threshold that triggers compaction is 16, not 10. Why that margin, and what problem would you have if you set the threshold at exactly 10?
See solution
If the threshold were exactly 10, by the time you detect compaction is needed, turn number 11 has already arrived and the model already responded without seeing turn 1 — because contextWindowLength = 10 had already excluded it from that call: you'd be reacting one step late, after something had already been lost. Also, with no margin, the routine would tend to fire on almost every new turn once past the limit, multiplying calls to the model and rounds of "summary of the summary." The margin (16 against 10) gives room for compaction to run while the window is still comfortable, and keeping 6 raw turns after every pass ensures a good stretch of conversation passes before it's needed again.
Why it works: leaving distance between the moment you detect a problem and the moment something actually gets lost is the same principle behind any threshold-based alert — arrive before the limit, not right at it.
Exercise 3 — Correct a colleague. A colleague tells you: "To reset customer X's memory, I changed their sessionKey to a new one — that cleared out their old history, that record doesn't exist anymore." What would you correct?
See solution
That changing the sessionKey doesn't delete anything — it only means the next query doesn't read the old history, because it looks under a different key. In Postgres Chat Memory, the previous sessionKey's rows keep existing in the table exactly as they were; they're still queryable (for example, for auditing) if someone looks them up with the old key. If they genuinely need the history to stop existing — not just for the agent to stop reading it — the correct operation is Delete Messages with the mode set to All Messages on that specific sessionKey, which does delete the rows.
Why it works: telling apart "what the agent sees" from "what exists in the database" is the same separation between scope and storage you worked through in lesson 1 of this module — resetting the context touches scope, not necessarily storage.
Summary and next step
You now have the two pieces this module was missing to handle conversations that genuinely get long: compacting — condensing the turns about to fall out of the window into a summary that does fit, following the same idea Anthropic documents as compaction — and resetting — dropping the accumulated history entirely when the case changed or closed, whether because the sessionKey changes on its own (if you designed the scope well in lesson 4) or by explicitly deleting with Delete Messages.
Before moving on you should be able to: build in n8n the sequence Get Many Messages → split into old/recent → summarize with an LLM Chain → Insert Messages with Override, to compact a long conversation without losing the data that matters; decide, given a scenario, whether it's better to summarize and continue or reset completely, using this lesson's signals and not intuition; and explain why changing the sessionKey isn't the same as deleting the saved history.
With this you close out the problem lesson 6 opened: what to do when there's too much history. Lesson 8's mini-project takes every piece from the module — scope and storage (lessons 3 and 4), multi-turn conversations (lesson 5), and now compacting and resetting (lessons 6 and 7) — and asks you to assemble them into a single agent that holds persistent per-user memory and doesn't degrade even as the conversation gets long.
Resources
- Effective context engineering for AI agents — Anthropic — the source for "compaction," the exact technique you implemented in this lesson: summarizing a conversation near the window's limit and reinitiating it with that summary.
- Chat Memory Manager node — n8n Docs — complete reference for the node you used to read (Get Many Messages), replace (Insert Messages with Override All Messages), and delete (Delete Messages) the saved history.
- Basic LLM Chain node — n8n Docs — the node that generated the summary from the old turns, with its own prompt.
- Postgres Chat Memory node — n8n Docs — reference for
sessionKeyand the persistent storage that makes both compacting and resetting possible without losing the rest of the history. - Summarization Chain node — n8n Docs — an alternative to Basic LLM Chain when the history to summarize is so long it's worth splitting into fragments (map-reduce) instead of sending it whole in a single prompt.