2025-12-09 17:51:05 +00:00
---
summary: "Session management rules, keys, and persistence for chats"
read_when:
- Modifying session handling or storage
2026-01-31 16:04:03 -05:00
title: "Session Management"
2025-12-09 17:51:05 +00:00
---
2026-01-31 21:13:13 +09:00
2025-12-05 22:33:09 +01:00
# Session Management
2026-01-30 03:15:10 +01:00
OpenClaw treats **one direct-chat session per agent** as primary. Direct chats collapse to `agent:<agentId>:<mainKey>` (default `main` ), while group/channel chats get their own keys. `session.mainKey` is honored.
2025-12-05 22:33:09 +01:00
2026-01-15 10:57:00 +00:00
Use `session.dmScope` to control how **direct messages** are grouped:
2026-01-31 21:13:13 +09:00
2026-01-15 10:57:00 +00:00
- `main` (default): all DMs share the main session for continuity.
- `per-peer` : isolate by sender id across channels.
- `per-channel-peer` : isolate by channel + sender (recommended for multi-user inboxes).
2026-01-28 11:41:28 +05:30
- `per-account-channel-peer` : isolate by account + channel + sender (recommended for multi-account inboxes).
2026-01-31 21:13:13 +09:00
Use `session.identityLinks` to map provider-prefixed peer ids to a canonical identity so the same person shares a DM session across channels when using `per-peer` , `per-channel-peer` , or `per-account-channel-peer` .
2026-01-15 10:57:00 +00:00
2026-02-06 09:35:57 -05:00
## Secure DM mode (recommended for multi-user setups)
2026-02-03 22:55:13 -08:00
2026-02-05 13:30:26 -08:00
> **Security Warning:** If your agent can receive DMs from **multiple people**, you should strongly consider enabling secure DM mode. Without it, all users share the same conversation context, which can leak private information between users.
2026-02-04 21:05:16 -08:00
**Example of the problem with default settings:**
2026-02-05 13:30:26 -08:00
- Alice (`<SENDER_A>` ) messages your agent about a private topic (for example, a medical appointment)
- Bob (`<SENDER_B>` ) messages your agent asking "What were we talking about?"
- Because both DMs share the same session, the model may answer Bob using Alice's prior context.
2026-02-04 21:05:16 -08:00
**The fix:** Set `dmScope` to isolate sessions per user:
2026-02-03 22:55:13 -08:00
```json5
// ~/.openclaw/openclaw.json
{
session: {
// Secure DM mode: isolate DM context per channel + sender.
dmScope: "per-channel-peer",
},
}
```
2026-02-04 21:05:16 -08:00
**When to enable this:**
- You have pairing approvals for more than one sender
- You use a DM allowlist with multiple entries
- You set `dmPolicy: "open"`
- Multiple phone numbers or accounts can message your agent
2026-02-03 22:55:13 -08:00
Notes:
2026-02-04 21:05:16 -08:00
- Default is `dmScope: "main"` for continuity (all DMs share the main session). This is fine for single-user setups.
2026-02-22 12:36:33 +01:00
- Local CLI onboarding writes `session.dmScope: "per-channel-peer"` by default when unset (existing explicit values are preserved).
2026-02-03 22:55:13 -08:00
- For multi-account inboxes on the same channel, prefer `per-account-channel-peer` .
- If the same person contacts you on multiple channels, use `session.identityLinks` to collapse their DM sessions into one canonical identity.
2026-02-05 13:30:26 -08:00
- You can verify your DM settings with `openclaw security audit` (see [security ](/cli/security )).
2026-02-03 22:55:13 -08:00
2025-12-13 16:33:22 +00:00
## Gateway is the source of truth
2026-01-31 21:13:13 +09:00
2026-01-30 03:15:10 +01:00
All session state is **owned by the gateway** (the “master” OpenClaw). UI clients (macOS app, WebChat, etc.) must query the gateway for session lists and token counts instead of reading local files.
2025-12-13 16:33:22 +00:00
- In **remote mode** , the session store you care about lives on the remote gateway host, not your Mac.
- Token counts shown in UIs come from the gateway’ s store fields (`inputTokens` , `outputTokens` , `totalTokens` , `contextTokens` ). Clients do not parse JSONL transcripts to “fix up” totals.
2025-12-07 00:05:38 +01:00
## Where state lives
2026-01-31 21:13:13 +09:00
2025-12-13 16:33:22 +00:00
- On the **gateway host** :
2026-01-30 03:15:10 +01:00
- Store file: `~/.openclaw/agents/<agentId>/sessions/sessions.json` (per agent).
- Transcripts: `~/.openclaw/agents/<agentId>/sessions/<SessionId>.jsonl` (Telegram topic sessions use `.../<SessionId>-topic-<threadId>.jsonl` ).
2025-12-07 00:05:38 +01:00
- The store is a map `sessionKey -> { sessionId, updatedAt, ... }` . Deleting entries is safe; they are recreated on demand.
2026-01-13 07:15:57 +00:00
- Group entries may include `displayName` , `channel` , `subject` , `room` , and `space` to label sessions in UIs.
2026-01-18 02:41:06 +00:00
- Session entries include `origin` metadata (label + routing hints) so UIs can explain where a session came from.
2026-01-30 03:15:10 +01:00
- OpenClaw does **not** read legacy Pi/Tau session folders.
2025-12-05 22:33:09 +01:00
2026-02-23 17:39:48 -05:00
## Maintenance
OpenClaw applies session-store maintenance to keep `sessions.json` and transcript artifacts bounded over time.
### Defaults
- `session.maintenance.mode` : `warn`
- `session.maintenance.pruneAfter` : `30d`
- `session.maintenance.maxEntries` : `500`
- `session.maintenance.rotateBytes` : `10mb`
- `session.maintenance.resetArchiveRetention` : defaults to `pruneAfter` (`30d` )
- `session.maintenance.maxDiskBytes` : unset (disabled)
- `session.maintenance.highWaterBytes` : defaults to `80%` of `maxDiskBytes` when budgeting is enabled
### How it works
Maintenance runs during session-store writes, and you can trigger it on demand with `openclaw sessions cleanup` .
- `mode: "warn"` : reports what would be evicted but does not mutate entries/transcripts.
- `mode: "enforce"` : applies cleanup in this order:
1. prune stale entries older than `pruneAfter`
2. cap entry count to `maxEntries` (oldest first)
3. archive transcript files for removed entries that are no longer referenced
4. purge old `*.deleted.<timestamp>` and `*.reset.<timestamp>` archives by retention policy
5. rotate `sessions.json` when it exceeds `rotateBytes`
6. if `maxDiskBytes` is set, enforce disk budget toward `highWaterBytes` (oldest artifacts first, then oldest sessions)
### Performance caveat for large stores
Large session stores are common in high-volume setups. Maintenance work is write-path work, so very large stores can increase write latency.
What increases cost most:
- very high `session.maintenance.maxEntries` values
- long `pruneAfter` windows that keep stale entries around
- many transcript/archive artifacts in `~/.openclaw/agents/<agentId>/sessions/`
- enabling disk budgets (`maxDiskBytes` ) without reasonable pruning/cap limits
What to do:
- use `mode: "enforce"` in production so growth is bounded automatically
- set both time and count limits (`pruneAfter` + `maxEntries` ), not just one
- set `maxDiskBytes` + `highWaterBytes` for hard upper bounds in large deployments
- keep `highWaterBytes` meaningfully below `maxDiskBytes` (default is 80%)
- run `openclaw sessions cleanup --dry-run --json` after config changes to verify projected impact before enforcing
- for frequent active sessions, pass `--active-key` when running manual cleanup
### Customize examples
Use a conservative enforce policy:
```json5
{
session: {
maintenance: {
mode: "enforce",
pruneAfter: "45d",
maxEntries: 800,
rotateBytes: "20mb",
resetArchiveRetention: "14d",
},
},
}
```
Enable a hard disk budget for the sessions directory:
```json5
{
session: {
maintenance: {
mode: "enforce",
maxDiskBytes: "1gb",
highWaterBytes: "800mb",
},
},
}
```
Tune for larger installs (example):
```json5
{
session: {
maintenance: {
mode: "enforce",
pruneAfter: "14d",
maxEntries: 2000,
rotateBytes: "25mb",
maxDiskBytes: "2gb",
highWaterBytes: "1.6gb",
},
},
}
```
Preview or force maintenance from CLI:
```bash
openclaw sessions cleanup --dry-run
openclaw sessions cleanup --enforce
```
2026-01-08 22:23:03 +01:00
## Session pruning
2026-01-31 21:13:13 +09:00
2026-01-30 03:15:10 +01:00
OpenClaw trims **old tool results** from the in-memory context right before LLM calls by default.
2026-01-07 18:03:35 +01:00
This does **not** rewrite JSONL history. See [/concepts/session-pruning ](/concepts/session-pruning ).
2026-01-12 07:39:44 +00:00
## Pre-compaction memory flush
2026-01-31 21:13:13 +09:00
2026-01-30 03:15:10 +01:00
When a session nears auto-compaction, OpenClaw can run a **silent memory flush**
2026-01-12 07:39:44 +00:00
turn that reminds the model to write durable notes to disk. This only runs when
the workspace is writable. See [Memory ](/concepts/memory ) and
[Compaction ](/concepts/compaction ).
2025-12-07 00:05:38 +01:00
## Mapping transports → session keys
2026-01-31 21:13:13 +09:00
2026-01-15 10:57:00 +00:00
- Direct chats follow `session.dmScope` (default `main` ).
- `main` : `agent:<agentId>:<mainKey>` (continuity across devices/channels).
- Multiple phone numbers and channels can map to the same agent main key; they act as transports into one conversation.
- `per-peer` : `agent:<agentId>:dm:<peerId>` .
- `per-channel-peer` : `agent:<agentId>:<channel>:dm:<peerId>` .
2026-01-28 11:41:28 +05:30
- `per-account-channel-peer` : `agent:<agentId>:<channel>:<accountId>:dm:<peerId>` (accountId defaults to `default` ).
2026-01-16 14:23:22 -06:00
- If `session.identityLinks` matches a provider-prefixed peer id (for example `telegram:123` ), the canonical key replaces `<peerId>` so the same person shares a session across channels.
2026-01-13 07:15:57 +00:00
- Group chats isolate state: `agent:<agentId>:<channel>:group:<id>` (rooms/channels use `agent:<agentId>:<channel>:channel:<id>` ).
2026-01-07 02:10:56 +00:00
- Telegram forum topics append `:topic:<threadId>` to the group id for isolation.
2026-01-06 18:25:52 +00:00
- Legacy `group:<id>` keys are still recognized for migration.
2026-01-13 07:15:57 +00:00
- Inbound contexts may still use `group:<id>` ; the channel is inferred from `Provider` and normalized to the canonical `agent:<agentId>:<channel>:group:<id>` form.
2026-01-03 23:44:38 +01:00
- Other sources:
- Cron jobs: `cron:<job.id>`
- Webhooks: `hook:<uuid>` (unless explicitly set by the hook)
2026-01-22 23:07:58 +00:00
- Node runs: `node-<nodeId>`
2025-12-05 22:33:09 +01:00
2026-01-18 06:37:30 +00:00
## Lifecycle
2026-01-31 21:13:13 +09:00
2026-01-18 06:37:30 +00:00
- Reset policy: sessions are reused until they expire, and expiry is evaluated on the next inbound message.
- Daily reset: defaults to **4:00 AM local time on the gateway host** . A session is stale once its last update is earlier than the most recent daily reset time.
- Idle reset (optional): `idleMinutes` adds a sliding idle window. When both daily and idle resets are configured, **whichever expires first** forces a new session.
2026-01-30 03:15:10 +01:00
- Legacy idle-only: if you set `session.idleMinutes` without any `session.reset` /`resetByType` config, OpenClaw stays in idle-only mode for backward compatibility.
2026-02-08 16:20:52 -08:00
- Per-type overrides (optional): `resetByType` lets you override the policy for `direct` , `group` , and `thread` sessions (thread = Slack/Discord threads, Telegram topics, Matrix threads when provided by the connector).
2026-01-21 13:10:31 -06:00
- Per-channel overrides (optional): `resetByChannel` overrides the reset policy for a channel (applies to all session types for that channel and takes precedence over `reset` /`resetByType` ).
2026-01-30 03:15:10 +01:00
- Reset triggers: exact `/new` or `/reset` (plus any extras in `resetTriggers` ) start a fresh session id and pass the remainder of the message through. `/new <model>` accepts a model alias, `provider/model` , or provider name (fuzzy match) to set the new session model. If `/new` or `/reset` is sent alone, OpenClaw runs a short “hello” greeting turn to confirm the reset.
2025-12-07 00:05:38 +01:00
- Manual reset: delete specific keys from the store or remove the JSONL transcript; the next message recreates them.
2026-01-16 21:27:44 +00:00
- Isolated cron jobs always mint a fresh `sessionId` per run (no idle reuse).
2025-12-05 22:33:09 +01:00
2026-01-03 23:44:38 +01:00
## Send policy (optional)
2026-01-31 21:13:13 +09:00
2026-01-03 23:44:38 +01:00
Block delivery for specific session types without listing individual ids.
```json5
{
session: {
sendPolicy: {
rules: [
2026-01-13 07:15:57 +00:00
{ action: "deny", match: { channel: "discord", chatType: "group" } },
2026-01-31 21:13:13 +09:00
{ action: "deny", match: { keyPrefix: "cron:" } },
2026-02-15 02:41:30 +00:00
// Match the raw session key (including the `agent:<id>:` prefix).
{ action: "deny", match: { rawKeyPrefix: "agent:main:discord:" } },
2026-01-03 23:44:38 +01:00
],
2026-01-31 21:13:13 +09:00
default: "allow",
},
},
2026-01-03 23:44:38 +01:00
}
```
Runtime override (owner only):
2026-01-31 21:13:13 +09:00
2026-01-03 23:44:38 +01:00
- `/send on` → allow for this session
- `/send off` → deny for this session
- `/send inherit` → clear override and use config rules
2026-01-31 21:13:13 +09:00
Send these as standalone messages so they register.
2026-01-03 23:44:38 +01:00
2025-12-07 18:49:55 +01:00
## Configuration (optional rename example)
2026-01-31 21:13:13 +09:00
2025-12-05 22:33:09 +01:00
```json5
2026-01-30 03:15:10 +01:00
// ~/.openclaw/openclaw.json
2025-12-05 22:33:09 +01:00
{
2025-12-24 00:22:57 +00:00
session: {
2026-01-31 21:13:13 +09:00
scope: "per-sender", // keep group keys separate
dmScope: "main", // DM continuity (set per-channel-peer/per-account-channel-peer for shared inboxes)
2026-01-16 14:23:22 -06:00
identityLinks: {
2026-01-31 21:13:13 +09:00
alice: ["telegram:123456789", "discord:987654321012345678"],
2026-01-16 14:23:22 -06:00
},
2026-01-18 06:37:30 +00:00
reset: {
// Defaults: mode=daily, atHour=4 (gateway host local time).
// If you also set idleMinutes, whichever expires first wins.
mode: "daily",
atHour: 4,
2026-01-31 21:13:13 +09:00
idleMinutes: 120,
2026-01-18 06:37:30 +00:00
},
resetByType: {
thread: { mode: "daily", atHour: 4 },
2026-02-08 16:20:52 -08:00
direct: { mode: "idle", idleMinutes: 240 },
2026-01-31 21:13:13 +09:00
group: { mode: "idle", idleMinutes: 120 },
2026-01-18 06:37:30 +00:00
},
2026-01-21 13:10:31 -06:00
resetByChannel: {
2026-01-31 21:13:13 +09:00
discord: { mode: "idle", idleMinutes: 10080 },
2026-01-21 13:10:31 -06:00
},
2025-12-24 00:22:57 +00:00
resetTriggers: ["/new", "/reset"],
2026-01-30 03:15:10 +01:00
store: "~/.openclaw/agents/{agentId}/sessions/sessions.json",
2026-01-06 18:25:52 +00:00
mainKey: "main",
2026-01-31 21:13:13 +09:00
},
2025-12-05 22:33:09 +01:00
}
```
2025-12-07 00:05:38 +01:00
## Inspecting
2026-01-31 21:13:13 +09:00
2026-01-30 03:15:10 +01:00
- `openclaw status` — shows store path and recent sessions.
- `openclaw sessions --json` — dumps every entry (filter with `--active <minutes>` ).
- `openclaw gateway call sessions.list --params '{}'` — fetch sessions from the running gateway (use `--url` /`--token` for remote gateway access).
2026-03-12 23:30:58 +00:00
- Send `/status` as a standalone message in chat to see whether the agent is reachable, how much of the session context is used, current thinking/fast/verbose toggles, and when your WhatsApp web creds were last refreshed (helps spot relink needs).
2026-01-15 01:09:21 +00:00
- Send `/context list` or `/context detail` to see what’ s in the system prompt and injected workspace files (and the biggest context contributors).
2026-02-24 04:02:18 +00:00
- Send `/stop` (or standalone abort phrases like `stop` , `stop action` , `stop run` , `stop openclaw` ) to abort the current run, clear queued followups for that session, and stop any sub-agent runs spawned from it (the reply includes the stopped count).
2026-01-07 18:12:17 +01:00
- Send `/compact` (optional instructions) as a standalone message to summarize older context and free up window space. See [/concepts/compaction ](/concepts/compaction ).
2025-12-07 00:05:38 +01:00
- JSONL transcripts can be opened directly to review full turns.
2025-12-05 22:33:09 +01:00
## Tips
2026-01-31 21:13:13 +09:00
2025-12-07 00:05:38 +01:00
- Keep the primary key dedicated to 1:1 traffic; let groups keep their own keys.
- When automating cleanup, delete individual keys instead of the whole store to preserve context elsewhere.
2026-01-18 02:41:06 +00:00
## Session origin metadata
2026-01-31 21:13:13 +09:00
2026-01-18 02:41:06 +00:00
Each session entry records where it came from (best-effort) in `origin` :
2026-01-31 21:13:13 +09:00
2026-01-18 02:41:06 +00:00
- `label` : human label (resolved from conversation label + group subject/channel)
- `provider` : normalized channel id (including extensions)
- `from` /`to` : raw routing ids from the inbound envelope
- `accountId` : provider account id (when multi-account)
- `threadId` : thread/topic id when the channel supports it
2026-01-31 21:13:13 +09:00
The origin fields are populated for direct messages, channels, and groups. If a
connector only updates delivery routing (for example, to keep a DM main session
fresh), it should still provide inbound context so the session keeps its
explainer metadata. Extensions can do this by sending `ConversationLabel` ,
`GroupSubject` , `GroupChannel` , `GroupSpace` , and `SenderName` in the inbound
context and calling `recordSessionMetaFromInbound` (or passing the same context
to `updateLastRoute` ).