diff --git a/docs/concepts/session-pruning.md b/docs/concepts/session-pruning.md index ba9f39f37f1..090a1bb6a47 100644 --- a/docs/concepts/session-pruning.md +++ b/docs/concepts/session-pruning.md @@ -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 follow‑up requests can reuse the freshly cached prompt instead of re-caching the full history again. -- **What it does not do:** pruning doesn’t add tokens or “double” costs; it only changes what gets cached on that first post‑TTL 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 aren’t 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 diff --git a/docs/concepts/session-tool.md b/docs/concepts/session-tool.md index e6f658ba723..8b0d55ae114 100644 --- a/docs/concepts/session-tool.md +++ b/docs/concepts/session-tool.md @@ -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 agent’s main key). -- Group chats use `agent:::group:` or `agent:::channel:` (pass the full key). -- Cron jobs use `cron:`. -- Hooks use `hook:` unless explicitly set. -- Node sessions use `node-` 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:::group:` -- group chat (pass the full key). +- `cron:` -- cron job session. +- `hook:` -- webhook session. +- `node-` -- 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 ping‑pong. - - Max turns is `session.agentToAgent.maxPingPongTurns` (0–5, default 5). -- Once the loop ends, OpenClaw runs the **agent‑to‑agent 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 + round‑1 reply + latest ping‑pong 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//`. 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::subagent:` 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 `/.openclaw/attachments//` and a +receipt with sha256 is returned. ACP runtime rejects attachments. -- Starts a new `agent::subagent:` 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 diff --git a/docs/concepts/session.md b/docs/concepts/session.md index 2f00325b730..d438444a38d 100644 --- a/docs/concepts/session.md +++ b/docs/concepts/session.md @@ -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::` (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::` | Shared by default (`dmScope: "main"`) | +| Group chats | `agent:::group:` | Isolated per group | +| Rooms/channels | `agent:::channel:` | Isolated per room | +| Cron jobs | `cron:` | Fresh session per run | +| Webhooks | `hook:` | Unless explicitly overridden | +| Node runs | `node-` | 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:` for per-topic isolation. -**Example of the problem with default settings:** +## DM scope and isolation -- Alice (``) messages your agent about a private topic (for example, a medical appointment) -- Bob (``) 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 + + +If your agent receives DMs from multiple people, you should enable DM isolation. +Without it, all users share the same conversation context. + + +**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::main` | Single-user, cross-device continuity | +| `per-peer` | `agent::direct:` | Multi-user, cross-channel identity | +| `per-channel-peer` | `agent:::direct:` | Multi-user inboxes (recommended) | +| `per-account-channel-peer` | `agent::::direct:` | 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 `` 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 gateway’s 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 ` 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//sessions/sessions.json` (per agent). -- Transcripts: `~/.openclaw/agents//sessions/.jsonl` (Telegram topic sessions use `.../-topic-.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//sessions/sessions.json` | Key-value map of session metadata | +| Transcripts | `~/.openclaw/agents//sessions/.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 `.../-topic-.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.` and `*.reset.` 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//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::` (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::direct:`. - - `per-channel-peer`: `agent:::direct:`. - - `per-account-channel-peer`: `agent::::direct:` (accountId defaults to `default`). - - If `session.identityLinks` matches a provider-prefixed peer id (for example `telegram:123`), the canonical key replaces `` so the same person shares a session across channels. -- Group chats isolate state: `agent:::group:` (rooms/channels use `agent:::channel:`). - - Telegram forum topics append `:topic:` to the group id for isolation. - - Legacy `group:` keys are still recognized for migration. -- Inbound contexts may still use `group:`; the channel is inferred from `Provider` and normalized to the canonical `agent:::group:` form. -- Other sources: - - Cron jobs: `cron:` (isolated) or custom `session:` (persistent) - - Webhooks: `hook:` (unless explicitly set by the hook) - - Node runs: `node-` - -## 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 ` 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::` 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 `) | +| `/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 `). -- `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 what’s 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).