One gateway, but dozens of conversations from many people across many apps — and maybe several different agent personas. How does a message find the RIGHT context and the RIGHT agent, without leaking your work chat into your family group? This capstone builds session routing, isolation, and the most-specific-wins agent router — then ties all seven lessons together.
Picture the gateway humming along. Your sister is asking it about a recipe over WhatsApp. Your coworker is asking it to summarize a doc in Slack. You're poking at it from your terminal. Three people, three apps, all talking to the same running process that holds one agent. What stops the agent from answering your coworker using your sister's recipe history — or worse, surfacing something private one person said to a different person entirely?
Here is the bug in its purest form. Suppose the gateway keeps exactly one conversation — one running transcript — and every incoming message is appended to it. Person A sends "remind me my bank PIN is 4471." Person B, a stranger in a different chat, asks "what was that number again?" The agent, reading one shared transcript, helpfully replies with A's PIN. That is a cross-user leak, and it is not hypothetical — it is the default behavior of any system that forgets to partition its conversational state.
A session is the unit of isolation. Think of it as a labeled folder: it holds the back-and-forth of one conversation and nothing else. Your terminal chat is a session; your sister's WhatsApp thread is a different session; your coworker's Slack thread is a third. The agent, when it answers a message, only ever sees the transcript of that message's session. The folders never touch.
So the whole lesson reduces to two routing questions, asked of every inbound message. First: which session does this message belong to? (Chapters 1–4.) Second: which agent persona should handle it? (Chapters 5–6.) Get both right and many people can share one gateway safely. Get either wrong and you either leak context or answer in the wrong voice. Let's start with the session.
If a session is a labeled folder, then routing a message is just computing its label — a string that names which folder it belongs in. OpenClaw calls this string the session key. Two messages with the same key share a session (and therefore share memory); two messages with different keys are isolated. The entire isolation story is "compute the right key."
And the key is computed from the message's origin — where it came from, not what it says. Origin means the combination of which channel it arrived on (WhatsApp, Slack, a cron timer), who sent it (the peer), and what kind of conversation it is. The default policy sorts every inbound into one of three buckets:
group:slack:eng-standup. Everyone in that room shares the room's context, which is what you want — the room is the conversation.For an ordinary direct chat the key collapses to something like agent:<agentId>:<mainKey> — it is namespaced by which agent persona owns it (more on personas in Chapter 5) and then by a main key that identifies the specific conversation. The point of the namespacing is that two different personas, even talking to the same person, keep separate folders. We'll build the persona half later; for now, focus on the main key.
Notice the discipline here: the key is a pure function of the message's metadata. Same channel, same peer, same kind → same key → same session → shared memory. Change any of those and you may land in a different folder. That determinism is what makes the system reasonable to think about — and it is exactly what we'll implement now. Let's write the keying function and watch the leak appear and disappear as we change the rule.
Groups are easy: the room is the conversation, so everyone in group:slack:eng shares one folder and that's correct. Cron is easy: every run is fresh. The hard case — the case that caused the Chapter 0 leak — is the direct message. A DM is one human talking privately to the bot, and getting its key wrong is how private context spills between people.
OpenClaw exposes the choice as a single setting, session.dmScope. It is a dial with three positions, and turning it changes how much a DM session is shared:
dm:shared. Maximum sharing, maximum leak. Alice's secrets surface in Bob's chat. This is the default that bites.dm:alice. Each person gets their own folder. But the same "alice" identity reaching you on two channels would merge — sometimes fine, sometimes a surprise.dm:whatsapp:alice. Each person gets a separate folder per app. Alice-on-WhatsApp and Alice-on-Telegram are distinct conversations, and nobody can ever read anyone else.Why is per-channel-peer the recommended setting rather than the looser ones? Because it is the position that gives each real conversation exactly one folder and no folder is shared between two humans. The (channel, peer) pair is the finest-grained natural identity of "this person, in this app" — and a person's context in WhatsApp genuinely is a different conversation from their context in Slack. Going finer would over-fragment; going coarser re-opens a leak.
dmScope only changes how direct messages are keyed. A group session is still keyed by its group regardless of dmScope. So you can lock down one-on-one chats to per-channel-peer — killing the cross-user leak — while a Slack channel full of teammates keeps sharing its one room context, exactly as it should. The two concerns are orthogonal.
The simulation below makes this visceral. Three people DM the same bot across two channels. Drag the dmScope dial and watch the messages collapse into sessions. Under shared every message pours into one red folder — that red is the leak. Under per-channel-peer each (channel, peer) gets its own clean lane.
Five direct messages from three people across two channels (left) flow into session folders (right). Move the dial: shared merges everyone into one folder, drawn red because it is a cross-user leak; per-peer splits by person; per-channel-peer splits by (channel, peer) — the recommended, leak-free choice.
session.dmScope = per-channel-peer. What happens to a busy Slack group channel that many teammates share?A session can't live only in memory. The gateway is a long-lived daemon (Lesson 1), but it does restart — a crash, a reboot, a deploy — and when it comes back, your conversations should still be there. So every session is also persisted to disk, in a place laid out per agent persona.
The path tells the whole story. Sessions live under ~/.openclaw/agents/<agentId>/sessions/ — notice the <agentId> in the path: each persona has its own session store, so two agents on one gateway never share a folder on disk any more than they share one in memory. Inside that directory two kinds of file live side by side:
sessions.json — the index: lightweight lifecycle metadata for every session (its key, when it started, when it was last touched). One small file you can read to list all conversations.<sessionId>.jsonl — the transcript: the actual back-and-forth, one JSON object per line (that's what JSONL means — JSON Lines). Append-only, so adding a message is just writing one more line; you never rewrite the file.Why split metadata from transcript? Because they're accessed differently. To answer "which sessions exist and which is stale?" you only need the small index — you don't want to load every full transcript. To replay one conversation you stream its one JSONL file line by line. Separating them keeps the common operation (listing) cheap and the heavy operation (replay) localized.
Each session carries three timestamps that drive its lifecycle: sessionStartedAt (when this conversation began), lastInteractionAt (the most recent message either way), and updatedAt (the last time the record was written at all). These are not decoration — the next chapter's pruning logic reads them to decide what's old, and the reset rules below read them to decide what's stale.
/new or /reset. The human can always force a clean slate with a command, the moment they want to start over.The throughline: a session is born (start timestamp), lives and accumulates lines (last-interaction timestamp ticks forward), and eventually resets — on a schedule, on idleness, or on command — getting a fresh transcript while the index keeps track. That lifecycle is what keeps a conversation both durable (survives restarts) and bounded (doesn't grow without limit). And "bounded" is the whole subject of Chapter 4.
sessions.json index from a separate <sessionId>.jsonl transcript per conversation?Persistence has a cost: anything you keep forever, you store forever. Without a counter-force, the sessions directory grows without bound — old abandoned chats pile up on disk, and a single very long conversation's transcript balloons until it's expensive to load and (worse) too big to fit in the model's context. So the gateway runs maintenance, a background sweep that keeps total state bounded. By default it runs in enforce mode — it doesn't just warn, it actually applies the limits.
Two limits do the work, and they attack two different failure modes:
Pruning is a blunt instrument — it throws the whole session away — and that's fine for a conversation nobody returns to. But you can't just truncate a long active conversation; chopping off the oldest 60% would amnesia the agent mid-relationship. That's where compaction comes in: instead of deleting old turns, a compaction pipeline summarizes them into a compact recap and keeps that recap plus the most recent turns. The conversation stays affordable in tokens without forgetting its gist.
maxEntries is really a context-budget cap wearing a storage disguise. Compaction is how you respect that budget without going senile.
Let's implement the sweep. You'll write maintain: drop the dead sessions, cap the giant ones, and return what survives — then watch a plot of entries-before versus entries-after, with the cap line that everything must sit under.
maxEntries but is still active. What does maintenance do — and why not just prune it?So far "the agent" has been singular — one brain behind the gateway. But the gateway can host several. OpenClaw calls each one an agent, and the right mental model is a persona: a fully separate personality with its own life, running inside the same daemon. Your terse coding assistant and your warm, chatty family helper can both live on one gateway and never bleed into each other.
What makes them truly separate is not just a different name or system prompt — it's that each agent gets its own isolated everything:
~/.openclaw/agents/<agentId>/:
agentDir — the files and working directory it operates in;sessions/ directory from Chapter 3, namespaced by <agentId>. Its conversations are its own.agents.list[], where each entry can also set its own tools, skills, and sandbox.
Now the namespacing from Chapter 1 clicks into place. Remember the DM key was agent:<agentId>:<mainKey>? The leading agent:<agentId> is exactly this isolation showing up in the key. Even if the same person on the same channel talks to two different personas, the two conversations get different keys because the agentId differs — separate folders, separate memories, separate auth. Persona isolation and session isolation are the same fence, seen from two sides.
This raises the obvious question. If one gateway hosts several personas, and a message arrives on some channel — which agent should handle it? That decision is the second great routing problem of this lesson, and it has its own elegant rule. Chapter 6.
One gateway, several personas, a message just arrived. To pick the agent, the gateway consults a set of bindings. A binding is a rule that maps some property of the inbound — a channel account, a particular person, a Discord guild, a Slack team — to an agent. "Anything from the eng Slack team → the work agent." "Anything from my mom → the family agent."
But bindings can overlap. A message from your mom in the family Slack team matches both "the family team → family agent" and (if it existed) "my mom → family agent." Which wins? The answer is the single most important rule in agent routing:
accountId:"*" — any account on a whole channel type.Read the ladder as a sentence: "Do I have a rule for this exact person? No — for the thread they're replying in? No — for their server-plus-role? their server? their team? their account? their channel? — then fall back to the default agent." The first "yes" wins and you stop. That stop-on-first-match, ordered most-specific-first, is the algorithm.
Let's implement the router. You'll walk the ladder in priority order and return the first match — the heart of multi-agent routing in a dozen lines.
peer binding (for that exact sender) and a team binding (for their whole workspace). Which agent handles it, and why?A persona doesn't always work alone. Sometimes the agent handling your chat needs to go do something slow in the background — crawl a long document, run a multi-step research task, draft a report — without freezing the conversation. For that it spawns a subagent: a helper agent run, launched by the main agent to handle a chunk of work and report back.
But here's the danger, and it connects straight back to Lesson 6. If a heavy subagent ran on the same path as your live chat, it would clog it — your "hi, are you there?" would sit behind a ten-minute crawl. The queue we built in Lesson 6 already solved exactly this kind of problem with lanes: separate channels of work that don't block each other.
And this is where the two halves of the whole series shake hands. Lesson 6 gave us concurrency control — how to run overlapping work without collisions or head-of-line blocking. Lessons 1–7 here gave us routing — how to send each message to the right session and the right agent. Subagents sit precisely at the seam: a routing decision (this persona spawns a helper for this task) that is realized by a concurrency mechanism (run it on the subagent lane).
So the picture is now complete in both dimensions. Vertically, a message is routed: to a session (the right context) and to an agent (the right persona). Horizontally, the work is scheduled: foreground chat stays snappy on its lane while subagents grind away on theirs. Routing decides where a message goes; lanes decide when and alongside what it runs. The capstone showcase, next, drives both at once.
Here is everything from this lesson in one live diagram. Configure a dmScope and a few bindings, then fire messages from different people, groups, and teams. For each one the playground shows you both routing answers at once: which session key it lands in (the right context), and which agent wins via the most-specific ladder (the right persona) — with the winning rule highlighted. This is the payoff that exercises the whole lesson together.
Pick a dmScope, then press the buttons to fire inbound messages. Each message resolves to a session key (which conversation folder) and an agent (which persona, via the most-specific-wins ladder). The matched routing rule is named. Watch a peer rule beat a team rule, and watch shared turn two people's DMs the same red key.
dmScope, groups by group, cron fresh, all namespaced by agent (Ch 1–2), then persisted, reset, and bounded (Ch 3–4). Which agent? — the most-specific-wins ladder over bindings, peer down to default (Ch 5–6). Get both right and many people share one gateway with no leaks and the right voice every time. Subagents then fan slow work onto their own lane (Ch 7).
You built an entire agent gateway. Here is how the seven lessons connect, each one standing on the last:
agent event fires: intake → context → inference → tools → reply.Read end to end: a daemon owns connections, exposed through a protocol, gated by trust, running an agent loop over carefully built context that streams back, with overlapping work serialized by a queue and every message routed to the right session and agent. Daemon → protocol → trust → loop → context/stream → queue → sessions/agents. That's the whole machine.
dmScope does and why per-channel-peer is recommended; how sessions persist, reset, and stay bounded; what makes an agent a real isolated persona; and the most-specific-wins ladder. Then zoom out and name all seven lessons and how they chain. If you can, you don't just understand sessions — you understand the entire agent gateway you built, end to end.