OpenClaw Sessions Explained: Routing, Isolation, and When They Reset

OpenClaw Sessions Explained: Routing, Isolation, and When They Reset

Most guides about AI agent memory start with the wrong question. They ask how to store a conversation. OpenClaw sessions answer a sharper one first: which conversation does this message even belong to? Get that wrong and your agent doesn’t just forget things. It hands one person’s private chat to someone else.

That is the part worth understanding before anything else. In OpenClaw, a session is a routing decision, not a container you fill up. The gateway looks at where a message came from and drops it into the right conversation. This post walks through how that routing works, the one setting you cannot leave on its default, when sessions reset, and where all of it actually lives on disk.

What a session actually is in OpenClaw

A session is one continuous conversation, plus everything OpenClaw tracks about it: the transcript, the context window, the compaction state, and the timestamps that decide when it expires. Every inbound message gets routed into a session based on its origin.

The important design choice is who owns that state. The gateway does. Not the terminal, not the Control UI, not your phone. Those are all clients that query the gateway for session data. So when you open the same conversation in the web UI and then in a coding harness, you are looking at one gateway-owned session from two windows, not two copies that drift apart.

This is different from how most agent frameworks work, and that difference is the whole story. In the OpenAI Agents SDK you write SQLiteSession("conversation_123") and pass that id yourself; different ids mean different histories. LangGraph does the same thing with a thread_id. You, the developer, decide the key. OpenClaw decides it for you, from context. Less boilerplate, but the logic moved out of your code and into config, so you have to know the config.

How OpenClaw routes messages to sessions

The routing rules are short enough to memorize. Message origin determines the session:

Source Behavior
Direct messages Shared session by default
Group chats Isolated per group
Rooms / channels Isolated per room
Cron jobs Fresh session per run
Webhooks Isolated per hook

Read that top row again, because it is the trap. Every group is walled off from every other group. Every cron run starts clean so yesterday’s scheduled job doesn’t bleed into today’s. But direct messages all pour into a single shared session by default. For a personal agent that only you talk to, that is exactly right: one rolling conversation that follows you across Telegram, WhatsApp, and the web UI. The moment a second human can DM your agent, it becomes a leak.

The one setting that matters most: DM isolation

Here is the failure in plain terms. You deploy an agent. You and a colleague both message it directly. With the default DM scope, your colleague’s next message lands in the same session as yours, and the agent answers with full memory of your private conversation. Nobody gets an error. It just quietly does the wrong thing.

The fix is one config key:

{
  session: {
    dmScope: "per-channel-peer", // isolate by channel + sender
  },
}

session.dmScope has four values, and the differences are real:

Value Behavior
main (default) All DMs share the main session
per-peer Isolate by sender, across channels
per-channel-peer Isolate by channel + sender (recommended)
per-account-channel-peer Isolate by account + channel + sender

The recommended value is per-channel-peer: it keys a session to both who is writing and which channel they used. If the same person messages you from two channels and you want those to share context, map their identities with session.identityLinks to one canonical peer id instead of loosening the scope. And don’t take my word that your setup is safe. Run openclaw security audit and let it check.

My take: this default is a reasonable choice for the personal-agent use case OpenClaw is built around, but it is a footgun for anyone running a shared bot. Treat dmScope as mandatory the instant more than one person can reach the agent. It is the single most consequential line in your session config.

Session lifecycle: when a session resets

By default, sessions never reset on their own. They keep the same sessionId and let compaction summarize old context as the conversation grows. That is the mode: "none" behavior, and for a lot of agents it is fine. When you do want freshness, you opt into one of three policies:

  • Daily reset (mode: "daily") rolls a new session at a set local hour, atHour, default 4. Freshness is measured from when the current sessionId started, not from the last time some metadata got written.
  • Idle reset (mode: "idle") starts a new session after idleMinutes of inactivity. Inactivity here means no real user or channel interaction. This is the subtle bit: heartbeat pings, cron runs, and exec system events do not count as activity, so they will not keep an idle session artificially alive.
  • Manual reset is /new or /reset in chat. /new <model> resets and switches the model in one move.

You can set a global policy and override it per chat type or per channel:

{
  session: {
    reset: { mode: "daily", atHour: 4 },
    resetByType: {
      group: { mode: "idle", idleMinutes: 120 },
      thread: { mode: "daily", atHour: 6 },
    },
    resetByChannel: {
      discord: { mode: "idle", idleMinutes: 10080 },
    },
  },
}

When both a daily and an idle policy apply, whichever expires first wins. And when a reset rolls the session, any queued system-event notices for the old session get discarded, so you don’t open a fresh conversation with stale background chatter prepended to it.

Where session state actually lives

None of this is magic; it is rows in SQLite. Each agent gets its own database:

  • Runtime session rows: ~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite
  • Archived transcript files: ~/.openclaw/agents/<agentId>/sessions/

Each row carries three timestamps that are easy to confuse, so keep them straight. sessionStartedAt is when the current sessionId began, and daily reset reads it. lastInteractionAt is the last real interaction, and idle reset reads it. updatedAt is just the last time the row changed for any reason, useful for listing and pruning but not authoritative for reset timing. If you ever debug a session that reset “too early” or “never,” these three fields are where you look.

Incognito sessions

OpenClaw has an off-the-record mode. From the Control UI’s New thread screen, turn on Incognito before you start, and that thread’s session entry, transcript, and compaction state stay in process memory instead of on disk. It vanishes when the gateway restarts. It does not run the automatic memory flush, and deleting or resetting it writes no transcript archive.

One honest caveat: incognito does not restrict the agent’s tools. If you explicitly ask it to save something, or a tool writes a file, that data can still land outside the incognito store. Incognito controls session storage, not the agent’s hands. It also protects the thread from other users on a multi-user gateway, but not from the gateway owner or the operator of the process, who can always watch live sessions.

Keeping storage bounded: session maintenance

Sessions accumulate. OpenClaw caps that with session.maintenance:

{
  session: {
    maintenance: {
      mode: "enforce",   // "enforce" applies cleanup; "warn" only reports
      pruneAfter: "30d",
      maxEntries: 500,
    },
  },
}

By default it enforces: entries older than 30 days get pruned, and the store caps at 500 entries. Two things worth knowing. Synthetic entries from cron, hooks, heartbeat, and sub-agents are allowed to age out, but durable pointers like group and thread sessions are preserved. And archived sessions are exempt from every automatic path, age pruning and entry caps included. If you want a conversation to survive forever, archive it. Preview any cleanup with openclaw sessions cleanup --dry-run before you let it run for real.

Inspecting and debugging sessions

Four commands cover almost everything:

Command Shows
openclaw status Session store path and recent activity
openclaw sessions --json All sessions (filter with --active <minutes>)
/status in chat Context usage, model, and toggles
/context list What is in the system prompt right now

When a session behaves strangely, start with openclaw sessions --json --active 60 to see what is live, then check the three timestamps on the row in question.

How this compares to OpenAI Agents SDK and LangGraph

The contrast makes OpenClaw’s model easier to hold in your head. In the OpenAI Agents SDK, a session is an object you construct with an id, like SQLiteSession("conversation_123"); you decide the key, and an in-memory database is the default unless you pass a file path. LangGraph builds persistence around threads: a thread_id ties turns together, a checkpointer (in-memory, SQLite, or Postgres) writes state after each step, and remembering a user across a new thread means wiring up a separate long-term store.

Both put the keying logic in your application code. OpenClaw moves it into routing config. The tradeoff is straightforward. Frameworks give you total control and total responsibility for getting the isolation right. OpenClaw gives you sane routing out of the box and one config surface to tune, at the cost of needing to know that dmScope default. Neither is better in the abstract. If you are hand-building an agent’s internals, you probably want the framework’s control. If you are running a gateway that fields real messages from real channels, you want routing that already exists and only needs configuring.

Frequently asked questions

Do OpenClaw sessions persist across a gateway restart?

Normal sessions do; they live as rows in the per-agent SQLite database. Incognito sessions do not, by design, because they are held in process memory and are dropped on restart.

Why is my agent mixing up two people’s DMs?

Because session.dmScope is on its default of main, which puts all direct messages in one shared session. Set it to per-channel-peer and run openclaw security audit to confirm the fix.

Does a cron job reuse my main conversation?

No. Cron jobs get a fresh session per run, so scheduled work never inherits or pollutes your interactive conversation.

What is the difference between resetting and archiving a session?

A reset rolls the conversation to a new sessionId so the agent starts fresh. Archiving shelves a session and makes it exempt from all automatic maintenance, so it stays around until you explicitly unarchive or delete it.

Will heartbeat or cron activity keep an idle session alive?

No. Idle reset only counts real user or channel interaction. System events like heartbeat, cron, and exec write metadata but do not extend idle or daily freshness.

The takeaway

Sessions are the layer most people skip past on their way to prompts and tools, and it is the layer that decides whether your agent is coherent and private or confused and leaky. OpenClaw’s model is simple once it clicks: the gateway routes each message to a session by origin, you own that behavior through config, and the timestamps in SQLite tell you exactly why anything reset. If you run anything more than a solo personal agent, set dmScope today, run openclaw security audit, and read your own openclaw sessions --json output so you know what your gateway is actually keeping. That ten-minute pass is the cheapest insurance in your whole setup.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *