Alice first messages an agent privately from Telegram, then from Slack. She also mentions it in a Discord project group and opens a thread beneath that group. If history is keyed only by the display name “Alice,” private conversations, team context, and thread work collapse together. If everything is isolated by channel, switching to Slack makes the agent act like a stranger.
This is not a prompt problem. Before the model sees history, the runtime needs a stable, explainable, persistent address that also controls concurrency. OpenClaw produces a route, whose central field is sessionKey. The key indexes session state and becomes the embedded run's session lane.
A route says where a message belongs. When Alice adds “do not edit generated files” while tools are running, the runtime must also decide whether the message belongs to this turn or a later one. Queue mode owns that temporal decision. Routing and queuing jointly preserve transcript causality.
Reading contract.By the end, you should be able to distinguish binding (target agent), DM scope (how direct identities collapse), identityLinks (explicit canonical peer mapping), sessionKey (stable address), sessionId (current instance at that address), and transcript (that instance's history). You should also be able to explain the distinct effects of steer, followup, collect, and interrupt.
Evidence boundary.This chapter stays on c549250, using resolve-route.ts, auto-reply queue source, and the official Session and Command queue contracts. Cross-session memory retrieval does not change the session key; part four handles it separately.
1. Route ownership is not a reply address
A channel reply target says where outbound content should be delivered. A route says who owns the run and history. They are often related but not identical. Channel docking can move a direct session's reply route to another linked channel without creating a session. Conversely, group and private work should retain different owners even when both eventually deliver through one app.
ResolvedAgentRoute returns agentId, channel, accountId, effective dmScope, sessionKey, mainSessionKey, lastRoutePolicy, and matchedBy. The diagnostic matchedBy explains whether a peer, parent peer, wildcard, guild+roles, guild, team, account, channel, or default rule won.
Logging only the final agentId is not enough to debug routing. Without the match source, an operator cannot tell whether configuration failed to match, a more specific rule won, or a thread inherited a parent binding.
2. Bindings choose the agent; session policy builds the key
Bindings are configuration-level route rules. They can target a channel, account, peer, guild, roles, or team to select an agent. The source indexes bindings by channel/account and evaluates more specific peer and group constraints before account, channel, and default fallbacks. This both avoids scanning every binding per message and makes precedence testable.
After agent selection, buildAgentSessionKey passes agent, main key, channel, account, peer kind/id, dmScope, and identityLinks into the session-key builder. Model provider is deliberately absent: switching providers should not silently create another conversation.
export function buildAgentSessionKey(params: {
agentId: string;
channel: string;
accountId?: string | null;
peer?: RoutePeer | null;
dmScope?: "main" | "per-peer" | "per-channel-peer" | "per-account-channel-peer";
identityLinks?: Record<string, string[]>;
}): string
The general rule is to choose the logical owner first, then derive the state address under a session-isolation policy. Hiding both decisions inside string concatenation makes privacy audits and migrations nearly impossible.
3. DM scope is a privacy policy
OpenClaw defaults to dmScope: "main", collapsing direct messages into the agent's main session. That is convenient for a single trusted personal user: Telegram and Slack continue one conversation. On a Gateway where multiple people can DM the agent, the same default lets Alice and Bob share a transcript.
| dmScope | Collapse dimensions | Use and risk |
|---|---|---|
main | All DMs → main session | Best single-user continuity; unsafe for unrelated users. |
per-peer | Canonical peer across channels | Cross-channel continuity requires trustworthy identityLinks. |
per-channel-peer | Channel + peer | Recommended multi-user isolation; channels do not auto-trust. |
per-account-channel-peer | Account + channel + peer | Strongest multi-account isolation, least continuity. |
The choice is not merely “should the agent remember me?” It is “which external identities may read the same private history?” Privacy belongs in the key, before the model sees anything.
4. identityLinks solve sameness, not authority
identityLinks can map a Telegram identity and a Slack identity to one canonical peer so per-peer yields one session. It is explicit normalization, not fuzzy display-name matching. Automatic merging would turn a convenience feature into an identity-confusion vulnerability.
Linked identities do not merge every conversation. Groups, rooms, and channels remain isolated. rememberAcrossConversations may retrieve relevant fragments from other private transcripts, but it neither changes keys nor combines transcripts. Identity, session, and retrieval are separate layers.
5. Threads are route dimensions and inheritance boundaries
A thread cannot be treated as cosmetic message metadata. Different threads under one group usually represent different tasks and need separate sessions. Yet a thread may inherit its agent binding from the parent peer, which is why route input can carry both peer and parentPeer.
matchedBy: "binding.peer.parent" makes that inheritance observable. Otherwise, the same selected agent would conceal whether the thread matched directly or fell back to its parent. Inheritance rules belong in diagnostics, not only in hidden fallbacks.
6. sessionKey is an address; sessionId is an instance
This is the chapter's most important distinction. sessionKey is the stable routing, storage, and concurrency address. sessionId identifies the current conversation instance at that address. /new, /reset, or daily/idle policy can rotate the id while preserving the key.
If reset changed the key, inbound routing would struggle to find the current conversation. If reset only erased transcript without rotating the id, delayed followups, approvals, and recovery claims could not tell whether they belonged before or after reset. The stable address preserves reachability; the rolling instance expires asynchronous work.
7. Four temporal semantics: steer, followup, collect, interrupt

When no run is active, a message can start immediately. With an active run, each mode defines a different causal contract:
- steer: the default. Current assistant tool calls finish, then pending messages enter the active runtime before the next LLM call. It does not cut an in-flight tool in half; unsupported steering falls back to followup.
- followup: leaves the active run unchanged and queues each message as a later agent turn.
- collect: also avoids steering but coalesces messages after a quiet window; different delivery routes drain separately.
- interrupt: aborts the current session run, then starts the newest message. It fits “stop, this direction is wrong,” not ordinary clarification.
resolveQueueSettings resolves inline choice, persisted session override, channel config, global config, then default steer. An explicit choice for this session appropriately outranks deployment defaults.
8. Steer waits for a safe tool boundary
Suppose the agent is deleting old build artifacts and the user adds “keep yesterday's archive.” Once the tool call has begun, new text cannot rewrite its submitted arguments. Arbitrary termination may leave partial side effects. OpenClaw therefore injects steering after the current assistant turn finishes its tool calls and before the next model call.
The transcript can then represent reality: assistant proposed tool → tool result → user steering → next assistant. The model's view and the external world's timing agree. A truly urgent stop uses interrupt so abortion is an explicit lifecycle action.
9. Session and global lanes prevent different collisions
Each embedded run enters a session:<key> lane, allowing at most one active writer for a session. It then enters the global main lane, where agents.defaults.maxConcurrent limits runs across sessions. One global mutex would let Alice's long task block everyone; only a global cap would permit concurrent writers in one transcript.
Queue mode decides what an inbound message does while its session is busy. Lanes decide which runs may execute simultaneously. Collect still needs a session lane. maxConcurrent=1 does not imply steer—it merely makes a second run wait.
10. Queued turns need cancellation identity
Followup and collect work cannot live in an anonymous array before activation. The Gateway retains a cancel identity for each client runId until the item runs, drops, or becomes part of an overflow summary. chat.abort can cancel a particular queued run. A session-scoped abort cancels authorized queued work before active work so queue drain cannot promote another turn while stopping.
Waiting is an authorization-bearing ownership state.Without requester and session identity on queued content, a multi-owner session cannot safely let one caller cancel only its own work.
11. The key stays, the session rolls, stale work expires

SessionEntry timestamps own different policy. sessionStartedAt marks when the current sessionId began and drives daily reset. lastInteractionAt advances on real user/channel interaction and drives idle reset. updatedAt records any row mutation, including bookkeeping, heartbeat, or cron work, and must not keep a session artificially fresh.
Reset archives the old transcript and gives the new id a new transcript. A system notice, approval followup, or detached completion pinned to the old sessionId must fail a rebind check if the key now points to B. Comparing only the key would deliver old-conversation work into the new conversation.
Incognito is a different storage mode: session row, transcript, and compaction state stay in process memory and vanish on Gateway restart. It does not restrict tool file writes or prevent the model provider from processing messages. “No session transcript on disk” is not “no external effect” or complete privacy.
12. Transcript is a causal ledger, not an editable array
Routing and queues ultimately protect transcript order. User message, assistant tool call, tool result, steering input, and final reply must persist in occurrence order. Reset, compaction, and repair can rewrite structure, but the session owner serializes writes.
Channel plugins should not append their own parallel JSONL truth, and two runs should not choose the same “last message” as parent. Current OpenClaw keeps runtime session rows and hot transcript in each agent's openclaw-agent.sqlite. Legacy JSON/JSONL is migration input or archival form, not a second live owner.
13. Diagnose space, time, and instance separately
| Symptom | Inspect first | Do not change first |
|---|---|---|
| The same user “forgot” | Route inputs, matchedBy, dmScope, identityLinks, sessionKey. | Do not immediately enlarge prompt or memory. |
| Different users share history | Whether multi-user DMs still collapse to main. | Do not ask the model to enforce privacy. |
| Clarification did not change the active tool | Queue mode, tool boundary, runtime steering support. | Do not assume steer aborts an in-flight tool. |
| Old work appears after reset | Old sessionId pin and rebind check. | Do not deliver by sessionKey alone. |
| All chats block one another | Global lane and maxConcurrent. | Do not remove per-session serialization. |
14. Six rules from routing and sessions
- Routing assigns space; queuing assigns time.Together they define transcript causality.
- Separate agent selection from key construction.Bindings choose the logical owner; DM policy chooses isolation.
- Encode privacy in the key.A prompt cannot repair an unsafe multi-user DM collapse.
- Separate stable address from rolling instance.sessionKey routes; sessionId expires asynchronous work.
- Inject steering at a safe runtime boundary.New text cannot rewrite side effects already submitted.
- Queued items have owners.Cancel, drop, and overflow summary must preserve requester/run identity.
Part four goes inside the selected session to separate workspace, bootstrap, system prompt, history, compaction, and memory. Routing guarantees which history belongs to the user; context assembly decides which parts are actually visible to the model in this run.
Source and documentation index
- resolve-route.ts: binding precedence, route result, and session-key construction.
- session-key.ts: agent session key parsing and special key shapes.
- queue/settings.ts: mode, debounce, cap, and drop precedence.
- get-reply-run-admission.ts: active run, steer/followup, and interrupt admission.
- Session management, Command queue, and Steering queue: public semantics.
