docs: rewrite session management, session pruning, and session tools pages

This commit is contained in:
Vincent Koc
2026-03-30 07:18:30 +09:00
parent f8dc4305a5
commit 2880b3d3ff
3 changed files with 439 additions and 445 deletions

View File

@@ -1,6 +1,6 @@
---
title: "Session Pruning"
summary: "Session pruning: tool-result trimming to reduce context bloat"
summary: "How session pruning trims old tool results to reduce context bloat and improve cache efficiency"
read_when:
- You want to reduce LLM context growth from tool outputs
- You are tuning agents.defaults.contextPruning
@@ -8,90 +8,102 @@ read_when:
# Session Pruning
Session pruning trims **old tool results** from the in-memory context right before each LLM call. It does **not** rewrite the on-disk session history (`*.jsonl`).
Session pruning trims **old tool results** from the in-memory context before
each LLM call. It does **not** rewrite the on-disk session history (JSONL) --
it only affects what gets sent to the model for that request.
## When it runs
## Why prune
- When `mode: "cache-ttl"` is enabled and the last Anthropic call for the session is older than `ttl`.
- Only affects the messages sent to the model for that request.
- Only active for Anthropic API calls (and OpenRouter Anthropic models).
- For best results, match `ttl` to your model `cacheRetention` policy (`short` = 5m, `long` = 1h).
- After a prune, the TTL window resets so subsequent requests keep cache until `ttl` expires again.
Long-running sessions accumulate tool outputs (exec results, file reads, search
results). These inflate the context window, increasing cost and eventually
forcing [compaction](/concepts/compaction). Pruning removes stale tool output so
the model sees a leaner context on each turn.
## Smart defaults (Anthropic)
Pruning is also important for **Anthropic prompt caching**. When a session goes
idle past the cache TTL, the next request re-caches the full prompt. Pruning
reduces the cache-write size for that first post-TTL request, which directly
reduces cost.
- **OAuth or setup-token** profiles: enable `cache-ttl` pruning and set heartbeat to `1h`.
- **API key** profiles: enable `cache-ttl` pruning, set heartbeat to `30m`, and default `cacheRetention: "short"` on Anthropic models.
- If you set any of these values explicitly, OpenClaw does **not** override them.
## How it works
## What this improves (cost + cache behavior)
Pruning runs in `cache-ttl` mode, which is the only supported mode:
- **Why prune:** Anthropic prompt caching only applies within the TTL. If a session goes idle past the TTL, the next request re-caches the full prompt unless you trim it first.
- **What gets cheaper:** pruning reduces the **cacheWrite** size for that first request after the TTL expires.
- **Why the TTL reset matters:** once pruning runs, the cache window resets, so followup requests can reuse the freshly cached prompt instead of re-caching the full history again.
- **What it does not do:** pruning doesnt add tokens or “double” costs; it only changes what gets cached on that first postTTL request.
1. **Check the clock** -- pruning only runs if the last Anthropic API call for
the session is older than `ttl` (default `5m`).
2. **Find prunable messages** -- only `toolResult` messages are eligible. User
and assistant messages are never modified.
3. **Protect recent context** -- the last `keepLastAssistants` assistant
messages (default `3`) and all tool results after that cutoff are preserved.
4. **Soft-trim** oversized tool results -- keep the head and tail, insert
`...`, and append a note with the original size.
5. **Hard-clear** remaining eligible results -- replace the entire content with
a placeholder.
6. **Reset the TTL** -- subsequent requests keep cache until `ttl` expires
again.
## What can be pruned
### What gets skipped
- Only `toolResult` messages.
- User + assistant messages are **never** modified.
- The last `keepLastAssistants` assistant messages are protected; tool results after that cutoff are not pruned.
- If there arent enough assistant messages to establish the cutoff, pruning is skipped.
- Tool results containing **image blocks** are skipped (never trimmed/cleared).
- Tool results containing **image blocks** are never trimmed.
- If there are not enough assistant messages to establish the cutoff, pruning
is skipped entirely.
- Pruning currently only activates for Anthropic API calls (and OpenRouter
Anthropic models).
## Context window estimation
## Smart defaults
Pruning uses an estimated context window (chars ≈ tokens × 4). The base window is resolved in this order:
OpenClaw auto-configures pruning for Anthropic profiles:
1. `models.providers.*.models[].contextWindow` override.
2. Model definition `contextWindow` (from the model registry).
3. Default `200000` tokens.
| Profile type | Pruning | Heartbeat | Cache retention |
| -------------------- | ------------------- | --------- | ------------------ |
| OAuth or setup-token | `cache-ttl` enabled | `1h` | (provider default) |
| API key | `cache-ttl` enabled | `30m` | `short` (5 min) |
If `agents.defaults.contextTokens` is set, it is treated as a cap (min) on the resolved window.
If you set any of these values explicitly, OpenClaw does not override them.
## Mode
Match `ttl` to your model `cacheRetention` policy for best results (`short` =
5 min, `long` = 1 hour).
### cache-ttl
## Pruning vs compaction
- Pruning only runs if the last Anthropic call is older than `ttl` (default `5m`).
- When it runs: same soft-trim + hard-clear behavior as before.
| | Pruning | Compaction |
| -------------- | --------------------------------- | ------------------------------- |
| **What** | Trims tool result messages | Summarizes conversation history |
| **Persisted?** | No (in-memory, per request) | Yes (in JSONL transcript) |
| **Scope** | Tool results only | Entire conversation |
| **Trigger** | Every LLM call (when TTL expired) | Context window threshold |
## Soft vs hard pruning
Built-in tools already truncate their own output. Pruning is an additional layer
that prevents long-running chats from accumulating too much tool output over
time. See [Compaction](/concepts/compaction) for the summarization approach.
- **Soft-trim**: only for oversized tool results.
- Keeps head + tail, inserts `...`, and appends a note with the original size.
- Skips results with image blocks.
- **Hard-clear**: replaces the entire tool result with `hardClear.placeholder`.
## Configuration
## Tool selection
### Defaults (when enabled)
- `tools.allow` / `tools.deny` support `*` wildcards.
- Deny wins.
- Matching is case-insensitive.
- Empty allow list => all tools allowed.
| Setting | Default | Description |
| ----------------------- | ----------------------------------- | ------------------------------------------------ |
| `ttl` | `5m` | Prune only after this idle period |
| `keepLastAssistants` | `3` | Protect tool results near recent assistant turns |
| `softTrimRatio` | `0.3` | Context ratio for soft-trim eligibility |
| `hardClearRatio` | `0.5` | Context ratio for hard-clear eligibility |
| `minPrunableToolChars` | `50000` | Minimum tool result size to consider |
| `softTrim.maxChars` | `4000` | Max chars after soft-trim |
| `softTrim.headChars` | `1500` | Head portion to keep |
| `softTrim.tailChars` | `1500` | Tail portion to keep |
| `hardClear.enabled` | `true` | Enable hard-clear stage |
| `hardClear.placeholder` | `[Old tool result content cleared]` | Replacement text |
## Interaction with other limits
### Examples
- Built-in tools already truncate their own output; session pruning is an extra layer that prevents long-running chats from accumulating too much tool output in the model context.
- Compaction is separate: compaction summarizes and persists, pruning is transient per request. See [/concepts/compaction](/concepts/compaction).
## Defaults (when enabled)
- `ttl`: `"5m"`
- `keepLastAssistants`: `3`
- `softTrimRatio`: `0.3`
- `hardClearRatio`: `0.5`
- `minPrunableToolChars`: `50000`
- `softTrim`: `{ maxChars: 4000, headChars: 1500, tailChars: 1500 }`
- `hardClear`: `{ enabled: true, placeholder: "[Old tool result content cleared]" }`
## Examples
Default (off):
Disable pruning (default state):
```json5
{
agents: { defaults: { contextPruning: { mode: "off" } } },
agents: {
defaults: {
contextPruning: { mode: "off" },
},
},
}
```
@@ -99,7 +111,11 @@ Enable TTL-aware pruning:
```json5
{
agents: { defaults: { contextPruning: { mode: "cache-ttl", ttl: "5m" } } },
agents: {
defaults: {
contextPruning: { mode: "cache-ttl", ttl: "5m" },
},
},
}
```
@@ -111,11 +127,32 @@ Restrict pruning to specific tools:
defaults: {
contextPruning: {
mode: "cache-ttl",
tools: { allow: ["exec", "read"], deny: ["*image*"] },
tools: {
allow: ["exec", "read"],
deny: ["*image*"],
},
},
},
},
}
```
See config reference: [Gateway Configuration](/gateway/configuration)
Tool selection supports `*` wildcards, deny wins over allow, matching is
case-insensitive, and an empty allow list means all tools are allowed.
## Context window estimation
Pruning estimates the context window (chars = tokens x 4). The base window is
resolved in this order:
1. `models.providers.*.models[].contextWindow` override.
2. Model definition `contextWindow` from the model registry.
3. Default `200000` tokens.
If `agents.defaults.contextTokens` is set, it caps the resolved window.
## Related
- [Compaction](/concepts/compaction) -- summarization-based context reduction
- [Session Management](/concepts/session) -- session lifecycle and routing
- [Gateway Configuration](/gateway/configuration) -- full config reference

View File

@@ -1,251 +1,220 @@
---
summary: "Agent session tools for listing sessions, fetching history, and sending cross-session messages"
summary: "Agent tools for listing sessions, reading history, cross-session messaging, and spawning sub-agents"
read_when:
- Adding or modifying session tools
- You want to understand agent session tools
- You are configuring cross-session access or sub-agent spawning
title: "Session Tools"
---
# Session Tools
Goal: small, hard-to-misuse tool set so agents can list sessions, fetch history, and send to another session.
OpenClaw gives agents a small set of tools to interact with sessions: list them,
read their history, send messages across sessions, and spawn isolated sub-agent
runs.
## Tool Names
## Overview
- `sessions_list`
- `sessions_history`
- `sessions_send`
- `sessions_spawn`
| Tool | Purpose |
| ------------------ | ----------------------------------- |
| `sessions_list` | List sessions with optional filters |
| `sessions_history` | Fetch transcript for one session |
| `sessions_send` | Send a message into another session |
| `sessions_spawn` | Spawn an isolated sub-agent session |
## Key Model
## Session keys
- Main direct chat bucket is always the literal key `"main"` (resolved to the current agents main key).
- Group chats use `agent:<agentId>:<channel>:group:<id>` or `agent:<agentId>:<channel>:channel:<id>` (pass the full key).
- Cron jobs use `cron:<job.id>`.
- Hooks use `hook:<uuid>` unless explicitly set.
- Node sessions use `node-<nodeId>` unless explicitly set.
Session tools use **session keys** to identify conversations:
`global` and `unknown` are reserved values and are never listed. If `session.scope = "global"`, we alias it to `main` for all tools so callers never see `global`.
- `"main"` -- the agent's main direct-chat session.
- `agent:<agentId>:<channel>:group:<id>` -- group chat (pass the full key).
- `cron:<job.id>` -- cron job session.
- `hook:<uuid>` -- webhook session.
- `node-<nodeId>` -- node session.
`global` and `unknown` are reserved and never listed. If
`session.scope = "global"`, it is aliased to `main` for all tools.
## sessions_list
List sessions as an array of rows.
Lists sessions as an array of rows.
Parameters:
**Parameters:**
- `kinds?: string[]` filter: any of `"main" | "group" | "cron" | "hook" | "node" | "other"`
- `limit?: number` max rows (default: server default, clamp e.g. 200)
- `activeMinutes?: number` only sessions updated within N minutes
- `messageLimit?: number` 0 = no messages (default 0); >0 = include last N messages
| Parameter | Type | Default | Description |
| --------------- | ---------- | -------------- | -------------------------------------------------------- |
| `kinds` | `string[]` | all | Filter: `main`, `group`, `cron`, `hook`, `node`, `other` |
| `limit` | `number` | server default | Max rows returned |
| `activeMinutes` | `number` | -- | Only sessions updated within N minutes |
| `messageLimit` | `number` | `0` | Include last N messages per session (0 = none) |
Behavior:
When `messageLimit > 0`, OpenClaw fetches chat history per session and includes
the last N messages. Tool results are filtered out in list output -- use
`sessions_history` for tool messages.
- `messageLimit > 0` fetches `chat.history` per session and includes the last N messages.
- Tool results are filtered out in list output; use `sessions_history` for tool messages.
- When running in a **sandboxed** agent session, session tools default to **spawned-only visibility** (see below).
Row shape (JSON):
- `key`: session key (string)
- `kind`: `main | group | cron | hook | node | other`
- `channel`: `whatsapp | telegram | discord | signal | imessage | webchat | internal | unknown`
- `displayName` (group display label if available)
- `updatedAt` (ms)
- `sessionId`
- `model`, `contextTokens`, `totalTokens`
- `thinkingLevel`, `verboseLevel`, `systemSent`, `abortedLastRun`
- `sendPolicy` (session override if set)
- `lastChannel`, `lastTo`
- `deliveryContext` (normalized `{ channel, to, accountId }` when available)
- `transcriptPath` (best-effort path derived from store dir + sessionId)
- `messages?` (only when `messageLimit > 0`)
**Row fields:** `key`, `kind`, `channel`, `displayName`, `updatedAt`,
`sessionId`, `model`, `contextTokens`, `totalTokens`, `thinkingLevel`,
`verboseLevel`, `sendPolicy`, `lastChannel`, `lastTo`, `deliveryContext`,
`transcriptPath`, and optionally `messages`.
## sessions_history
Fetch transcript for one session.
Fetches the transcript for one session.
Parameters:
**Parameters:**
- `sessionKey` (required; accepts session key or `sessionId` from `sessions_list`)
- `limit?: number` max messages (server clamps)
- `includeTools?: boolean` (default false)
| Parameter | Type | Default | Description |
| -------------- | --------- | -------------- | ----------------------------------------------- |
| `sessionKey` | `string` | required | Session key or `sessionId` from `sessions_list` |
| `limit` | `number` | server default | Max messages |
| `includeTools` | `boolean` | `false` | Include `toolResult` messages |
Behavior:
When given a `sessionId`, OpenClaw resolves it to the corresponding session key.
- `includeTools=false` filters `role: "toolResult"` messages.
- Returns messages array in the raw transcript format.
- When given a `sessionId`, OpenClaw resolves it to the corresponding session key (missing ids error).
### Gateway APIs
## Gateway session history and live transcript APIs
Control UI and gateway clients can use lower-level APIs directly:
Control UI and gateway clients can use the lower level history and live transcript surfaces directly.
HTTP:
- `GET /sessions/{sessionKey}/history`
- Query params: `limit`, `cursor`, `includeTools=1`, `follow=1`
- Unknown sessions return HTTP `404` with `error.type = "not_found"`
- `follow=1` upgrades the response to an SSE stream of transcript updates for that session
WebSocket:
- `sessions.subscribe` subscribes to all session lifecycle and transcript events visible to the client
- `sessions.messages.subscribe { key }` subscribes only to `session.message` events for one session
- `sessions.messages.unsubscribe { key }` removes that targeted transcript subscription
- `session.message` carries appended transcript messages plus live usage metadata when available
- `sessions.changed` emits `phase: "message"` for transcript appends so session lists can refresh counters and previews
- **HTTP:** `GET /sessions/{sessionKey}/history` with query params `limit`,
`cursor`, `includeTools=1`, `follow=1` (upgrades to SSE stream).
- **WebSocket:** `sessions.subscribe` for all lifecycle events,
`sessions.messages.subscribe { key }` for one session's transcript,
`sessions.messages.unsubscribe { key }` to remove.
## sessions_send
Send a message into another session.
Sends a message into another session.
Parameters:
**Parameters:**
- `sessionKey` (required; accepts session key or `sessionId` from `sessions_list`)
- `message` (required)
- `timeoutSeconds?: number` (default >0; 0 = fire-and-forget)
| Parameter | Type | Default | Description |
| ---------------- | -------- | -------- | ---------------------------------- |
| `sessionKey` | `string` | required | Target session key or `sessionId` |
| `message` | `string` | required | Message content |
| `timeoutSeconds` | `number` | > 0 | Wait timeout (0 = fire-and-forget) |
Behavior:
**Behavior:**
- `timeoutSeconds = 0`: enqueue and return `{ runId, status: "accepted" }`.
- `timeoutSeconds > 0`: wait up to N seconds for completion, then return `{ runId, status: "ok", reply }`.
- If wait times out: `{ runId, status: "timeout", error }`. Run continues; call `sessions_history` later.
- If the run fails: `{ runId, status: "error", error }`.
- Announce delivery runs after the primary run completes and is best-effort; `status: "ok"` does not guarantee the announce was delivered.
- Waits via gateway `agent.wait` (server-side) so reconnects don't drop the wait.
- Agent-to-agent message context is injected for the primary run.
- Inter-session messages are persisted with `message.provenance.kind = "inter_session"` so transcript readers can distinguish routed agent instructions from external user input.
- After the primary run completes, OpenClaw runs a **reply-back loop**:
- Round 2+ alternates between requester and target agents.
- Reply exactly `REPLY_SKIP` to stop the pingpong.
- Max turns is `session.agentToAgent.maxPingPongTurns` (05, default 5).
- Once the loop ends, OpenClaw runs the **agenttoagent announce step** (target agent only):
- Reply exactly `ANNOUNCE_SKIP` to stay silent.
- Any other reply is sent to the target channel.
- Announce step includes the original request + round1 reply + latest pingpong reply.
- `timeoutSeconds = 0` -- enqueue and return `{ runId, status: "accepted" }`.
- `timeoutSeconds > 0` -- wait for completion, then return the reply.
- Timeout: `{ runId, status: "timeout" }`. The run continues; check
`sessions_history` later.
## Channel Field
### Reply-back loop
- For groups, `channel` is the channel recorded on the session entry.
- For direct chats, `channel` maps from `lastChannel`.
- For cron/hook/node, `channel` is `internal`.
- If missing, `channel` is `unknown`.
After the target session responds, OpenClaw runs an alternating reply loop
between requester and target agents:
## Security / Send Policy
- Reply `REPLY_SKIP` to stop the ping-pong.
- Max turns: `session.agentToAgent.maxPingPongTurns` (0--5, default 5).
Policy-based blocking by channel/chat type (not per session id).
After the loop, an **announce step** posts the result to the target's chat
channel. Reply `ANNOUNCE_SKIP` to stay silent. The announce includes the
original request, round-1 reply, and latest ping-pong reply.
```json
{
"session": {
"sendPolicy": {
"rules": [
{
"match": { "channel": "discord", "chatType": "group" },
"action": "deny"
}
],
"default": "allow"
}
}
}
```
Runtime override (per session entry):
- `sendPolicy: "allow" | "deny"` (unset = inherit config)
- Settable via `sessions.patch` or owner-only `/send on|off|inherit` (standalone message).
Enforcement points:
- `chat.send` / `agent` (gateway)
- auto-reply delivery logic
Inter-session messages are tagged with
`message.provenance.kind = "inter_session"` so transcript readers can
distinguish routed agent instructions from external user input.
## sessions_spawn
Spawn an isolated delegated session.
Spawns an isolated delegated session for background work.
- Default runtime: OpenClaw sub-agent (`runtime: "subagent"`).
- ACP harness sessions use `runtime: "acp"` and follow ACP-specific targeting/policy rules.
- This section focuses on sub-agent behavior unless noted otherwise. For ACP-specific behavior, see [ACP Agents](/tools/acp-agents).
**Parameters:**
Parameters:
| Parameter | Type | Default | Description |
| ------------------- | --------- | ---------- | -------------------------------------------- |
| `task` | `string` | required | Task description |
| `runtime` | `string` | `subagent` | `subagent` or `acp` |
| `label` | `string` | -- | Label for logs/UI |
| `agentId` | `string` | -- | Target agent or ACP harness ID |
| `model` | `string` | -- | Override sub-agent model |
| `thinking` | `string` | -- | Override thinking level |
| `runTimeoutSeconds` | `number` | `0` | Abort after N seconds (0 = no limit) |
| `thread` | `boolean` | `false` | Request thread-bound routing |
| `mode` | `string` | `run` | `run` or `session` (session requires thread) |
| `cleanup` | `string` | `keep` | `delete` or `keep` |
| `sandbox` | `string` | `inherit` | `inherit` or `require` |
| `attachments` | `array` | -- | Inline files (subagent only) |
- `task` (required)
- `runtime?` (`subagent|acp`; defaults to `subagent`)
- `label?` (optional; used for logs/UI)
- `agentId?` (optional)
- `runtime: "subagent"`: target another OpenClaw agent id if allowed by `subagents.allowAgents`
- `runtime: "acp"`: target an ACP harness id if allowed by `acp.allowedAgents`
- `model?` (optional; overrides the sub-agent model; invalid values error)
- `thinking?` (optional; overrides thinking level for the sub-agent run)
- `runTimeoutSeconds?` (defaults to `agents.defaults.subagents.runTimeoutSeconds` when set, otherwise `0`; when set, aborts the sub-agent run after N seconds)
- `thread?` (default false; request thread-bound routing for this spawn when supported by the channel/plugin)
- `mode?` (`run|session`; defaults to `run`, but defaults to `session` when `thread=true`; `mode="session"` requires `thread=true`)
- `cleanup?` (`delete|keep`, default `keep`)
- `sandbox?` (`inherit|require`, default `inherit`; `require` rejects spawn unless the target child runtime is sandboxed)
- `attachments?` (optional array of inline files; subagent runtime only, ACP rejects). Each entry: `{ name, content, encoding?: "utf8" | "base64", mimeType? }`. Files are materialized into the child workspace at `.openclaw/attachments/<uuid>/`. Returns a receipt with sha256 per file.
- `attachAs?` (optional; `{ mountPath? }` hint reserved for future mount implementations)
**Behavior:**
Allowlist:
- Always non-blocking: returns `{ status: "accepted", runId, childSessionKey }`.
- Creates a new `agent:<agentId>:subagent:<uuid>` session with
`deliver: false`.
- Sub-agents get the full tool set minus session tools (configurable via
`tools.subagents.tools`).
- Sub-agents cannot call `sessions_spawn` (no recursive spawning).
- After completion, an announce step posts the result to the requester's
channel. Reply `ANNOUNCE_SKIP` to stay silent.
- Sub-agent sessions are auto-archived after
`agents.defaults.subagents.archiveAfterMinutes` (default: 60).
- `runtime: "subagent"`: `agents.list[].subagents.allowAgents` controls which OpenClaw agent ids are allowed via `agentId` (`["*"]` to allow any). Default: only the requester agent.
- `runtime: "acp"`: `acp.allowedAgents` controls which ACP harness ids are allowed. This is a separate policy from `subagents.allowAgents`.
- Sandbox inheritance guard: if the requester session is sandboxed, `sessions_spawn` rejects targets that would run unsandboxed.
### Allowlists
Discovery:
- **Subagent:** `agents.list[].subagents.allowAgents` controls which agent IDs
are allowed (`["*"]` for any). Default: only the requester.
- **ACP:** `acp.allowedAgents` controls allowed ACP harness IDs (separate from
subagent policy).
- If the requester is sandboxed, targets that would run unsandboxed are
rejected.
- Use `agents_list` to discover allowed targets for `runtime: "subagent"`.
- For `runtime: "acp"`, use configured ACP harness ids and `acp.allowedAgents`; `agents_list` does not list ACP harness targets.
### Attachments
Behavior:
Each entry: `{ name, content, encoding?: "utf8" | "base64", mimeType? }`.
Files are materialized into `<workspace>/.openclaw/attachments/<uuid>/` and a
receipt with sha256 is returned. ACP runtime rejects attachments.
- Starts a new `agent:<agentId>:subagent:<uuid>` session with `deliver: false`.
- Sub-agents default to the full tool set **minus session tools** (configurable via `tools.subagents.tools`).
- Sub-agents are not allowed to call `sessions_spawn` (no sub-agent → sub-agent spawning).
- Always non-blocking: returns `{ status: "accepted", runId, childSessionKey }` immediately.
- With `thread=true`, channel plugins can bind delivery/routing to a thread target (Discord support is controlled by `session.threadBindings.*` and `channels.discord.threadBindings.*`).
- After completion, OpenClaw runs a sub-agent **announce step** and posts the result to the requester chat channel.
- If the assistant final reply is empty, the latest `toolResult` from sub-agent history is included as `Result`.
- Reply exactly `ANNOUNCE_SKIP` during the announce step to stay silent.
- Announce replies are normalized to `Status`/`Result`/`Notes`; `Status` comes from runtime outcome (not model text).
- Sub-agent sessions are auto-archived after `agents.defaults.subagents.archiveAfterMinutes` (default: 60).
- Announce replies include a stats line (runtime, tokens, sessionKey/sessionId, transcript path, and optional cost).
For ACP-specific behavior (harness targeting, permission modes), see
[ACP Agents](/tools/acp-agents).
## Sandbox Session Visibility
## Visibility and access control
Session tools can be scoped to reduce cross-session access.
Session tools can be scoped to limit cross-session access.
Default behavior:
### Visibility levels
- `tools.sessions.visibility` defaults to `tree` (current session + spawned subagent sessions).
- For sandboxed sessions, `agents.defaults.sandbox.sessionToolsVisibility` can hard-clamp visibility.
| Level | What the agent can see |
| ---------------- | ------------------------------------------------------- |
| `self` | Only the current session |
| `tree` (default) | Current session + spawned sub-agent sessions |
| `agent` | Any session belonging to the current agent |
| `all` | Any session (cross-agent requires `tools.agentToAgent`) |
Config:
Configure at `tools.sessions.visibility`.
### Sandbox clamping
Sandboxed sessions have an additional clamp via
`agents.defaults.sandbox.sessionToolsVisibility` (default: `spawned`). When
this is set, visibility is clamped to `tree` even if
`tools.sessions.visibility = "all"`.
```json5
{
tools: {
sessions: {
// "self" | "tree" | "agent" | "all"
// default: "tree"
visibility: "tree",
},
},
agents: {
defaults: {
sandbox: {
// default: "spawned"
sessionToolsVisibility: "spawned", // or "all"
sessionToolsVisibility: "spawned",
},
},
},
}
```
Notes:
## Send policy
- `self`: only the current session key.
- `tree`: current session + sessions spawned by the current session.
- `agent`: any session belonging to the current agent id.
- `all`: any session (cross-agent access still requires `tools.agentToAgent`).
- When a session is sandboxed and `sessionToolsVisibility="spawned"`, OpenClaw clamps visibility to `tree` even if you set `tools.sessions.visibility="all"`.
Policy-based blocking by channel or chat type prevents agents from sending to
restricted sessions. See [Session Management](/concepts/session) for send policy
configuration.
## Related
- [Session Management](/concepts/session) -- session routing, lifecycle, and
maintenance
- [ACP Agents](/tools/acp-agents) -- ACP-specific spawning and permissions
- [Multi-agent](/concepts/multi-agent) -- multi-agent architecture

View File

@@ -1,126 +1,195 @@
---
summary: "Session management rules, keys, and persistence for chats"
summary: "How OpenClaw manages sessions -- routing, isolation, lifecycle, and maintenance"
read_when:
- Modifying session handling or storage
- You want to understand session keys and routing
- You want to configure DM isolation or multi-user setups
- You want to tune session lifecycle or maintenance
title: "Session Management"
---
# Session Management
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.
OpenClaw manages conversations through **sessions**. Each session has a key
(which conversation bucket it belongs to), an ID (which transcript file
continues it), and metadata tracked in a session store.
Use `session.dmScope` to control how **direct messages** are grouped:
## How sessions are routed
- `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).
- `per-account-channel-peer`: isolate by account + channel + sender (recommended for multi-account inboxes).
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`.
Every inbound message is mapped to a **session key** that determines which
conversation it joins:
## Secure DM mode (recommended for multi-user setups)
| Source | Session key pattern | Behavior |
| --------------- | ---------------------------------------- | ------------------------------------- |
| Direct messages | `agent:<agentId>:<mainKey>` | Shared by default (`dmScope: "main"`) |
| Group chats | `agent:<agentId>:<channel>:group:<id>` | Isolated per group |
| Rooms/channels | `agent:<agentId>:<channel>:channel:<id>` | Isolated per room |
| Cron jobs | `cron:<job.id>` | Fresh session per run |
| Webhooks | `hook:<uuid>` | Unless explicitly overridden |
| Node runs | `node-<nodeId>` | Unless explicitly overridden |
> **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.
Telegram forum topics append `:topic:<threadId>` for per-topic isolation.
**Example of the problem with default settings:**
## DM scope and isolation
- 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.
By default, all direct messages share one session (`dmScope: "main"`) for
continuity across devices and channels. This works well for single-user setups,
but can leak context when multiple people message your agent.
**The fix:** Set `dmScope` to isolate sessions per user:
### Secure DM mode
<Warning>
If your agent receives DMs from multiple people, you should enable DM isolation.
Without it, all users share the same conversation context.
</Warning>
**The problem:** Alice messages about a private topic. Bob asks "What were we
talking about?" Because both share a session, the model may answer Bob using
Alice's context.
**The fix:**
```json5
// ~/.openclaw/openclaw.json
{
session: {
// Secure DM mode: isolate DM context per channel + sender.
dmScope: "per-channel-peer",
},
}
```
**When to enable this:**
### DM scope options
- 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
| Value | Key pattern | Best for |
| -------------------------- | -------------------------------------------------- | ------------------------------------ |
| `main` (default) | `agent:<id>:main` | Single-user, cross-device continuity |
| `per-peer` | `agent:<id>:direct:<peerId>` | Multi-user, cross-channel identity |
| `per-channel-peer` | `agent:<id>:<channel>:direct:<peerId>` | Multi-user inboxes (recommended) |
| `per-account-channel-peer` | `agent:<id>:<channel>:<accountId>:direct:<peerId>` | Multi-account inboxes |
Notes:
### Cross-channel identity linking
- Default is `dmScope: "main"` for continuity (all DMs share the main session). This is fine for single-user setups.
- Local CLI onboarding writes `session.dmScope: "per-channel-peer"` by default when unset (existing explicit values are preserved).
- 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.
- You can verify your DM settings with `openclaw security audit` (see [security](/cli/security)).
When using `per-peer` or `per-channel-peer`, the same person messaging from
different channels gets separate sessions. Use `session.identityLinks` to
collapse them:
## Gateway is the source of truth
```json5
{
session: {
identityLinks: {
alice: ["telegram:123456789", "discord:987654321012345678"],
},
},
}
```
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.
The canonical key replaces `<peerId>` so Alice shares one session across
channels.
- 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 gateways store fields (`inputTokens`, `outputTokens`, `totalTokens`, `contextTokens`). Clients do not parse JSONL transcripts to “fix up” totals.
**When to enable DM isolation:**
- Pairing approvals for more than one sender
- DM allowlist with multiple entries
- `dmPolicy: "open"`
- Multiple phone numbers or accounts can message the agent
Verify settings with `openclaw security audit` (see [security](/cli/security)).
Local CLI onboarding writes `per-channel-peer` by default when unset.
## Session lifecycle
### Resets
Sessions are reused until they expire. Expiry is evaluated on the next inbound
message:
- **Daily reset** (default) -- 4:00 AM local time on the gateway host. A
session is stale once its last update is before the most recent reset time.
- **Idle reset** (optional) -- `idleMinutes` adds a sliding idle window.
- **Combined** -- when both are configured, whichever expires first forces a new
session.
Override per session type or channel:
```json5
{
session: {
reset: {
mode: "daily",
atHour: 4,
idleMinutes: 120,
},
resetByType: {
thread: { mode: "daily", atHour: 4 },
direct: { mode: "idle", idleMinutes: 240 },
group: { mode: "idle", idleMinutes: 120 },
},
resetByChannel: {
discord: { mode: "idle", idleMinutes: 10080 },
},
},
}
```
### Manual resets
- `/new` or `/reset` starts a fresh session. The remainder of the message is
passed through.
- `/new <model>` accepts a model alias, `provider/model`, or provider name
(fuzzy match) to set the session model.
- If sent alone, OpenClaw runs a short greeting turn to confirm the reset.
- Custom triggers: add to `resetTriggers` array.
- Delete specific keys from the store or remove the JSONL transcript; the next
message recreates them.
- Isolated cron jobs always mint a fresh `sessionId` per run.
## Where state lives
- On the **gateway host**:
- 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`).
- The store is a map `sessionKey -> { sessionId, updatedAt, ... }`. Deleting entries is safe; they are recreated on demand.
- Group entries may include `displayName`, `channel`, `subject`, `room`, and `space` to label sessions in UIs.
- Session entries include `origin` metadata (label + routing hints) so UIs can explain where a session came from.
- OpenClaw does **not** read legacy Pi/Tau session folders.
All session state is **owned by the gateway**. UI clients (macOS app, WebChat,
TUI) query the gateway for session lists and token counts.
## Maintenance
In remote mode, the session store lives on the remote gateway host, not your
local machine.
OpenClaw applies session-store maintenance to keep `sessions.json` and transcript artifacts bounded over time.
### Storage
| Artifact | Path | Purpose |
| ------------- | --------------------------------------------------------- | --------------------------------- |
| Session store | `~/.openclaw/agents/<agentId>/sessions/sessions.json` | Key-value map of session metadata |
| Transcripts | `~/.openclaw/agents/<agentId>/sessions/<sessionId>.jsonl` | Append-only conversation history |
The store maps `sessionKey -> { sessionId, updatedAt, ... }`. Deleting entries
is safe; they are recreated on demand. Group entries may include `displayName`,
`channel`, `subject`, `room`, and `space` for UI labeling.
Telegram topic sessions use `.../<sessionId>-topic-<threadId>.jsonl`.
## Session maintenance
OpenClaw keeps the session store and transcripts 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
| Setting | Default | Description |
| ----------------------- | ------------------- | --------------------------------------------------------------- |
| `mode` | `warn` | `warn` reports what would be evicted; `enforce` applies cleanup |
| `pruneAfter` | `30d` | Stale-entry age cutoff |
| `maxEntries` | `500` | Cap entries in sessions.json |
| `rotateBytes` | `10mb` | Rotate sessions.json when oversized |
| `resetArchiveRetention` | `30d` | Retention for reset archives |
| `maxDiskBytes` | unset | Optional sessions-directory budget |
| `highWaterBytes` | 80% of maxDiskBytes | Target after cleanup |
### How it works
### Enforcement order (`mode: "enforce"`)
Maintenance runs during session-store writes, and you can trigger it on demand with `openclaw sessions cleanup`.
1. Prune stale entries older than `pruneAfter`.
2. Cap entry count to `maxEntries` (oldest first).
3. Archive transcript files for removed entries.
4. Purge old reset/deleted archives by retention policy.
5. Rotate `sessions.json` when it exceeds `rotateBytes`.
6. If `maxDiskBytes` is set, enforce disk budget toward `highWaterBytes`.
- `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)
### Configuration examples
### 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:
Conservative enforce policy:
```json5
{
@@ -136,7 +205,7 @@ Use a conservative enforce policy:
}
```
Enable a hard disk budget for the sessions directory:
Hard disk budget:
```json5
{
@@ -150,75 +219,26 @@ Enable a hard disk budget for the sessions directory:
}
```
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:
Preview or force from CLI:
```bash
openclaw sessions cleanup --dry-run
openclaw sessions cleanup --enforce
```
## Session pruning
### Performance note
OpenClaw trims **old tool results** from the in-memory context right before LLM calls by default.
This does **not** rewrite JSONL history. See [/concepts/session-pruning](/concepts/session-pruning).
Large session stores can increase write-path latency. To keep things fast:
## Pre-compaction memory flush
- Use `mode: "enforce"` in production.
- Set both time and count limits (`pruneAfter` + `maxEntries`).
- Set `maxDiskBytes` + `highWaterBytes` for hard upper bounds.
- Run `openclaw sessions cleanup --dry-run --json` after config changes to
preview impact.
When a session nears auto-compaction, OpenClaw can run a **silent memory flush**
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).
## Send policy
## Mapping transports → session keys
- 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>:direct:<peerId>`.
- `per-channel-peer`: `agent:<agentId>:<channel>:direct:<peerId>`.
- `per-account-channel-peer`: `agent:<agentId>:<channel>:<accountId>:direct:<peerId>` (accountId defaults to `default`).
- 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.
- Group chats isolate state: `agent:<agentId>:<channel>:group:<id>` (rooms/channels use `agent:<agentId>:<channel>:channel:<id>`).
- Telegram forum topics append `:topic:<threadId>` to the group id for isolation.
- Legacy `group:<id>` keys are still recognized for migration.
- Inbound contexts may still use `group:<id>`; the channel is inferred from `Provider` and normalized to the canonical `agent:<agentId>:<channel>:group:<id>` form.
- Other sources:
- Cron jobs: `cron:<job.id>` (isolated) or custom `session:<custom-id>` (persistent)
- Webhooks: `hook:<uuid>` (unless explicitly set by the hook)
- Node runs: `node-<nodeId>`
## Lifecycle
- 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.
- Legacy idle-only: if you set `session.idleMinutes` without any `session.reset`/`resetByType` config, OpenClaw stays in idle-only mode for backward compatibility.
- 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).
- 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`).
- 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.
- Manual reset: delete specific keys from the store or remove the JSONL transcript; the next message recreates them.
- Isolated cron jobs always mint a fresh `sessionId` per run (no idle reuse).
## Send policy (optional)
Block delivery for specific session types without listing individual ids.
Block delivery for specific session types without listing individual IDs:
```json5
{
@@ -227,7 +247,6 @@ Block delivery for specific session types without listing individual ids.
rules: [
{ action: "deny", match: { channel: "discord", chatType: "group" } },
{ action: "deny", match: { keyPrefix: "cron:" } },
// Match the raw session key (including the `agent:<id>:` prefix).
{ action: "deny", match: { rawKeyPrefix: "agent:main:discord:" } },
],
default: "allow",
@@ -238,73 +257,42 @@ Block delivery for specific session types without listing individual ids.
Runtime override (owner only):
- `/send on` allow for this session
- `/send off` deny for this session
- `/send inherit` clear override and use config rules
Send these as standalone messages so they register.
- `/send on` -- allow for this session.
- `/send off` -- deny for this session.
- `/send inherit` -- clear override and use config rules.
## Configuration (optional rename example)
## Inspecting sessions
```json5
// ~/.openclaw/openclaw.json
{
session: {
scope: "per-sender", // keep group keys separate
dmScope: "main", // DM continuity (set per-channel-peer/per-account-channel-peer for shared inboxes)
identityLinks: {
alice: ["telegram:123456789", "discord:987654321012345678"],
},
reset: {
// Defaults: mode=daily, atHour=4 (gateway host local time).
// If you also set idleMinutes, whichever expires first wins.
mode: "daily",
atHour: 4,
idleMinutes: 120,
},
resetByType: {
thread: { mode: "daily", atHour: 4 },
direct: { mode: "idle", idleMinutes: 240 },
group: { mode: "idle", idleMinutes: 120 },
},
resetByChannel: {
discord: { mode: "idle", idleMinutes: 10080 },
},
resetTriggers: ["/new", "/reset"],
store: "~/.openclaw/agents/{agentId}/sessions/sessions.json",
mainKey: "main",
},
}
```
| Method | What it shows |
| ------------------------------------ | ---------------------------------------------------------- |
| `openclaw status` | Store path, recent sessions |
| `openclaw sessions --json` | All entries (filter with `--active <minutes>`) |
| `/status` in chat | Reachability, context usage, toggles, cred freshness |
| `/context list` or `/context detail` | System prompt contents, biggest context contributors |
| `/stop` in chat | Abort current run, clear queued followups, stop sub-agents |
## Inspecting
- `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).
- 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).
- Send `/context list` or `/context detail` to see whats in the system prompt and injected workspace files (and the biggest context contributors).
- 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).
- Send `/compact` (optional instructions) as a standalone message to summarize older context and free up window space. See [/concepts/compaction](/concepts/compaction).
- JSONL transcripts can be opened directly to review full turns.
## Tips
- 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.
JSONL transcripts can be opened directly to review full turns.
## Session origin metadata
Each session entry records where it came from (best-effort) in `origin`:
- `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
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`).
- `label` -- human label (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 (multi-account).
- `threadId` -- thread/topic ID when supported.
Extensions populate these by sending `ConversationLabel`, `GroupSubject`,
`GroupChannel`, `GroupSpace`, and `SenderName` in the inbound context.
## Tips
- 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.
- Related: [Session Pruning](/concepts/session-pruning),
[Compaction](/concepts/compaction),
[Session Tools](/concepts/session-tool),
[Session Management Deep Dive](/reference/session-management-compaction).