chore: Run pnpm format:fix.

This commit is contained in:
cpojer
2026-01-31 21:13:13 +09:00
parent dcc2de15a6
commit 8cab78abbc
624 changed files with 10729 additions and 7514 deletions

View File

@@ -3,6 +3,7 @@ summary: "Agent loop lifecycle, streams, and wait semantics"
read_when:
- You need an exact walkthrough of the agent loop or lifecycle events
---
# Agent Loop (OpenClaw)
An agentic loop is the full “real” run of an agent: intake → context assembly → model inference →
@@ -14,53 +15,61 @@ as the model thinks, calls tools, and streams output. This doc explains how that
wired end-to-end.
## Entry points
- Gateway RPC: `agent` and `agent.wait`.
- CLI: `agent` command.
## How it works (high-level)
1) `agent` RPC validates params, resolves session (sessionKey/sessionId), persists session metadata, returns `{ runId, acceptedAt }` immediately.
2) `agentCommand` runs the agent:
1. `agent` RPC validates params, resolves session (sessionKey/sessionId), persists session metadata, returns `{ runId, acceptedAt }` immediately.
2. `agentCommand` runs the agent:
- resolves model + thinking/verbose defaults
- loads skills snapshot
- calls `runEmbeddedPiAgent` (pi-agent-core runtime)
- emits **lifecycle end/error** if the embedded loop does not emit one
3) `runEmbeddedPiAgent`:
3. `runEmbeddedPiAgent`:
- serializes runs via per-session + global queues
- resolves model + auth profile and builds the pi session
- subscribes to pi events and streams assistant/tool deltas
- enforces timeout -> aborts run if exceeded
- returns payloads + usage metadata
4) `subscribeEmbeddedPiSession` bridges pi-agent-core events to OpenClaw `agent` stream:
4. `subscribeEmbeddedPiSession` bridges pi-agent-core events to OpenClaw `agent` stream:
- tool events => `stream: "tool"`
- assistant deltas => `stream: "assistant"`
- lifecycle events => `stream: "lifecycle"` (`phase: "start" | "end" | "error"`)
5) `agent.wait` uses `waitForAgentJob`:
5. `agent.wait` uses `waitForAgentJob`:
- waits for **lifecycle end/error** for `runId`
- returns `{ status: ok|error|timeout, startedAt, endedAt, error? }`
## Queueing + concurrency
- Runs are serialized per session key (session lane) and optionally through a global lane.
- This prevents tool/session races and keeps session history consistent.
- Messaging channels can choose queue modes (collect/steer/followup) that feed this lane system.
See [Command Queue](/concepts/queue).
## Session + workspace preparation
- Workspace is resolved and created; sandboxed runs may redirect to a sandbox workspace root.
- Skills are loaded (or reused from a snapshot) and injected into env and prompt.
- Bootstrap/context files are resolved and injected into the system prompt report.
- A session write lock is acquired; `SessionManager` is opened and prepared before streaming.
## Prompt assembly + system prompt
- System prompt is built from OpenClaws base prompt, skills prompt, bootstrap context, and per-run overrides.
- Model-specific limits and compaction reserve tokens are enforced.
- See [System prompt](/concepts/system-prompt) for what the model sees.
## Hook points (where you can intercept)
OpenClaw has two hook systems:
- **Internal hooks** (Gateway hooks): event-driven scripts for commands and lifecycle events.
- **Plugin hooks**: extension points inside the agent/tool lifecycle and gateway pipeline.
### Internal hooks (Gateway hooks)
- **`agent:bootstrap`**: runs while building bootstrap files before the system prompt is finalized.
Use this to add/remove bootstrap context files.
- **Command hooks**: `/new`, `/reset`, `/stop`, and other command events (see Hooks doc).
@@ -68,7 +77,9 @@ OpenClaw has two hook systems:
See [Hooks](/hooks) for setup and examples.
### Plugin hooks (agent + gateway lifecycle)
These run inside the agent loop or gateway pipeline:
- **`before_agent_start`**: inject context or override system prompt before the run starts.
- **`agent_end`**: inspect the final message list and run metadata after completion.
- **`before_compaction` / `after_compaction`**: observe or annotate compaction cycles.
@@ -81,17 +92,20 @@ These run inside the agent loop or gateway pipeline:
See [Plugins](/plugin#plugin-hooks) for the hook API and registration details.
## Streaming + partial replies
- Assistant deltas are streamed from pi-agent-core and emitted as `assistant` events.
- Block streaming can emit partial replies either on `text_end` or `message_end`.
- Reasoning streaming can be emitted as a separate stream or as block replies.
- See [Streaming](/concepts/streaming) for chunking and block reply behavior.
## Tool execution + messaging tools
- Tool start/update/end events are emitted on the `tool` stream.
- Tool results are sanitized for size and image payloads before logging/emitting.
- Messaging tool sends are tracked to suppress duplicate assistant confirmations.
## Reply shaping + suppression
- Final payloads are assembled from:
- assistant text (and optional reasoning)
- inline tool summaries (when verbose + allowed)
@@ -102,24 +116,29 @@ See [Plugins](/plugin#plugin-hooks) for the hook API and registration details.
(unless a messaging tool already sent a user-visible reply).
## Compaction + retries
- Auto-compaction emits `compaction` stream events and can trigger a retry.
- On retry, in-memory buffers and tool summaries are reset to avoid duplicate output.
- See [Compaction](/concepts/compaction) for the compaction pipeline.
## Event streams (today)
- `lifecycle`: emitted by `subscribeEmbeddedPiSession` (and as a fallback by `agentCommand`)
- `assistant`: streamed deltas from pi-agent-core
- `tool`: streamed tool events from pi-agent-core
## Chat channel handling
- Assistant deltas are buffered into chat `delta` messages.
- A chat `final` is emitted on **lifecycle end/error**.
## Timeouts
- `agent.wait` default: 30s (just the wait). `timeoutMs` param overrides.
- Agent runtime: `agents.defaults.timeoutSeconds` default 600s; enforced in `runEmbeddedPiAgent` abort timer.
## Where things can end early
- Agent timeout (abort)
- AbortSignal (cancel)
- Gateway disconnect or RPC timeout

View File

@@ -4,6 +4,7 @@ read_when:
- You need to explain the agent workspace or its file layout
- You want to back up or migrate an agent workspace
---
# Agent workspace
The workspace is the agent's home. It is the only working directory used for
@@ -29,8 +30,8 @@ inside a sandbox workspace under `~/.openclaw/sandboxes`, not your host workspac
```json5
{
agent: {
workspace: "~/.openclaw/workspace"
}
workspace: "~/.openclaw/workspace",
},
}
```

View File

@@ -3,6 +3,7 @@ summary: "Agent runtime (embedded pi-mono), workspace contract, and session boot
read_when:
- Changing agent runtime, workspace bootstrap, or session behavior
---
# Agent Runtime 🤖
OpenClaw runs a single embedded agent runtime derived from **pi-mono**.
@@ -22,6 +23,7 @@ per-session workspaces under `agents.defaults.sandbox.workspaceRoot` (see
## Bootstrap files (injected)
Inside `agents.defaults.workspace`, OpenClaw expects these user-editable files:
- `AGENTS.md` — operating instructions + “memory”
- `SOUL.md` — persona, boundaries, tone
- `TOOLS.md` — user-maintained tool notes (e.g. `imsg`, `sag`, conventions)
@@ -48,11 +50,12 @@ To disable bootstrap file creation entirely (for pre-seeded workspaces), set:
Core tools (read/exec/edit/write and related system tools) are always available,
subject to tool policy. `apply_patch` is optional and gated by
`tools.exec.applyPatch`. `TOOLS.md` does **not** control which tools exist; its
guidance for how *you* want them used.
guidance for how _you_ want them used.
## Skills
OpenClaw loads skills from three locations (workspace wins on name conflict):
- Bundled (shipped with the install)
- Managed/local: `~/.openclaw/skills`
- Workspace: `<workspace>/skills`
@@ -69,6 +72,7 @@ OpenClaw reuses pieces of the pi-mono codebase (models/tools), but **session man
## Sessions
Session transcripts are stored as JSONL at:
- `~/.openclaw/agents/<agentId>/sessions/<SessionId>.jsonl`
The session ID is stable and chosen by OpenClaw.
@@ -109,9 +113,10 @@ Model refs in config (for example `agents.defaults.model` and `agents.defaults.m
## Configuration (minimal)
At minimum, set:
- `agents.defaults.workspace`
- `channels.whatsapp.allowFrom` (strongly recommended)
---
*Next: [Group Chats](/concepts/group-messages)* 🦞
_Next: [Group Chats](/concepts/group-messages)_ 🦞

View File

@@ -3,6 +3,7 @@ summary: "WebSocket gateway architecture, components, and client flows"
read_when:
- Working on gateway protocol, clients, or transports
---
# Gateway architecture
Last updated: 2026-01-22
@@ -22,26 +23,31 @@ Last updated: 2026-01-22
## Components and flows
### Gateway (daemon)
- Maintains provider connections.
- Exposes a typed WS API (requests, responses, serverpush events).
- Validates inbound frames against JSON Schema.
- Emits events like `agent`, `chat`, `presence`, `health`, `heartbeat`, `cron`.
### Clients (mac app / CLI / web admin)
- One WS connection per client.
- Send requests (`health`, `status`, `send`, `agent`, `system-presence`).
- Subscribe to events (`tick`, `agent`, `presence`, `shutdown`).
### Nodes (macOS / iOS / Android / headless)
- Connect to the **same WS server** with `role: node`.
- Provide a device identity in `connect`; pairing is **devicebased** (role `node`) and
approval lives in the device pairing store.
- Expose commands like `canvas.*`, `camera.*`, `screen.record`, `location.get`.
Protocol details:
- [Gateway protocol](/gateway/protocol)
### WebChat
- Static UI that uses the Gateway WS API for chat history and sends.
- In remote setups, connects through the same SSH/Tailscale tunnel as other
clients.

View File

@@ -3,8 +3,8 @@ summary: "Routing rules per channel (WhatsApp, Telegram, Discord, Slack) and sha
read_when:
- Changing channel routing or inbox behavior
---
# Channels & routing
# Channels & routing
OpenClaw routes replies **back to the channel where a message came from**. The
model does not choose a channel; routing is deterministic and controlled by the
@@ -62,8 +62,8 @@ Config:
broadcast: {
strategy: "parallel",
"120363403215116621@g.us": ["alfred", "baerbel"],
"+15555550123": ["support", "logger"]
}
"+15555550123": ["support", "logger"],
},
}
```
@@ -79,14 +79,12 @@ Example:
```json5
{
agents: {
list: [
{ id: "support", name: "Support", workspace: "~/.openclaw/workspace-support" }
]
list: [{ id: "support", name: "Support", workspace: "~/.openclaw/workspace-support" }],
},
bindings: [
{ match: { channel: "slack", teamId: "T123" }, agentId: "support" },
{ match: { channel: "telegram", peer: { kind: "group", id: "-100123" } }, agentId: "support" }
]
{ match: { channel: "telegram", peer: { kind: "group", id: "-100123" } }, agentId: "support" },
],
}
```
@@ -108,6 +106,7 @@ agent in one place.
## Reply context
Inbound replies include:
- `ReplyToId`, `ReplyToBody`, and `ReplyToSender` when available.
- Quoted context is appended to `Body` as a `[Replying to ...]` block.

View File

@@ -4,24 +4,30 @@ read_when:
- You want to understand auto-compaction and /compact
- You are debugging long sessions hitting context limits
---
# Context Window & Compaction
Every model has a **context window** (max tokens it can see). Long-running chats accumulate messages and tool results; once the window is tight, OpenClaw **compacts** older history to stay within limits.
## What compaction is
Compaction **summarizes older conversation** into a compact summary entry and keeps recent messages intact. The summary is stored in the session history, so future requests use:
- The compaction summary
- Recent messages after the compaction point
Compaction **persists** in the sessions JSONL history.
## Configuration
See [Compaction config & modes](/concepts/compaction) for the `agents.defaults.compaction` settings.
## Auto-compaction (default on)
When a session nears or exceeds the models context window, OpenClaw triggers auto-compaction and may retry the original request using the compacted context.
Youll see:
- `🧹 Auto-compaction complete` in verbose mode
- `/status` showing `🧹 Compactions: <count>`
@@ -29,21 +35,26 @@ Before compaction, OpenClaw can run a **silent memory flush** turn to store
durable notes to disk. See [Memory](/concepts/memory) for details and config.
## Manual compaction
Use `/compact` (optionally with instructions) to force a compaction pass:
```
/compact Focus on decisions and open questions
```
## Context window source
Context window is model-specific. OpenClaw uses the model definition from the configured provider catalog to determine limits.
## Compaction vs pruning
- **Compaction**: summarises and **persists** in JSONL.
- **Session pruning**: trims old **tool results** only, **in-memory**, per request.
See [/concepts/session-pruning](/concepts/session-pruning) for pruning details.
## Tips
- Use `/compact` when sessions feel stale or context is bloated.
- Large tool outputs are already truncated; pruning can further reduce tool-result buildup.
- If you need a fresh slate, `/new` or `/reset` starts a new session id.

View File

@@ -5,16 +5,18 @@ read_when:
- You are debugging why the model “knows” something (or forgot it)
- You want to reduce context overhead (/context, /status, /compact)
---
# Context
“Context” is **everything OpenClaw sends to the model for a run**. It is bounded by the models **context window** (token limit).
Beginner mental model:
- **System prompt** (OpenClaw-built): rules, tools, skills list, time/runtime, and injected workspace files.
- **Conversation history**: your messages + the assistants messages for this session.
- **Tool calls/results + attachments**: command output, file reads, images/audio, etc.
Context is *not the same thing* as “memory”: memory can be stored on disk and reloaded later; context is whats inside the models current window.
Context is _not the same thing_ as “memory”: memory can be stored on disk and reloaded later; context is whats inside the models current window.
## Quick start (inspect context)
@@ -76,6 +78,7 @@ Top tools (schema size):
## What counts toward the context window
Everything the model receives counts, including:
- System prompt (all sections).
- Conversation history.
- Tool calls + tool results.
@@ -86,6 +89,7 @@ Everything the model receives counts, including:
## How OpenClaw builds the system prompt
The system prompt is **OpenClaw-owned** and rebuilt each run. It includes:
- Tool list + short descriptions.
- Skills list (metadata only; see below).
- Workspace location.
@@ -98,6 +102,7 @@ Full breakdown: [System Prompt](/concepts/system-prompt).
## Injected workspace files (Project Context)
By default, OpenClaw injects a fixed set of workspace files (if present):
- `AGENTS.md`
- `SOUL.md`
- `TOOLS.md`
@@ -112,19 +117,21 @@ Large files are truncated per-file using `agents.defaults.bootstrapMaxChars` (de
The system prompt includes a compact **skills list** (name + description + location). This list has real overhead.
Skill instructions are *not* included by default. The model is expected to `read` the skills `SKILL.md` **only when needed**.
Skill instructions are _not_ included by default. The model is expected to `read` the skills `SKILL.md` **only when needed**.
## Tools: there are two costs
Tools affect context in two ways:
1) **Tool list text** in the system prompt (what you see as “Tooling”).
2) **Tool schemas** (JSON). These are sent to the model so it can call tools. They count toward context even though you dont see them as plain text.
1. **Tool list text** in the system prompt (what you see as “Tooling”).
2. **Tool schemas** (JSON). These are sent to the model so it can call tools. They count toward context even though you dont see them as plain text.
`/context detail` breaks down the biggest tool schemas so you can see what dominates.
## Commands, directives, and “inline shortcuts”
Slash commands are handled by the Gateway. There are a few different behaviors:
- **Standalone commands**: a message that is only `/...` runs as a command.
- **Directives**: `/think`, `/verbose`, `/reasoning`, `/elevated`, `/model`, `/queue` are stripped before the model sees the message.
- Directive-only messages persist session settings.
@@ -136,15 +143,17 @@ Details: [Slash commands](/tools/slash-commands).
## Sessions, compaction, and pruning (what persists)
What persists across messages depends on the mechanism:
- **Normal history** persists in the session transcript until compacted/pruned by policy.
- **Compaction** persists a summary into the transcript and keeps recent messages intact.
- **Pruning** removes old tool results from the *in-memory* prompt for a run, but does not rewrite the transcript.
- **Pruning** removes old tool results from the _in-memory_ prompt for a run, but does not rewrite the transcript.
Docs: [Session](/concepts/session), [Compaction](/concepts/compaction), [Session pruning](/concepts/session-pruning).
## What `/context` actually reports
`/context` prefers the latest **run-built** system prompt report when available:
- `System prompt (run)` = captured from the last embedded (tool-capable) run and persisted in the session store.
- `System prompt (estimate)` = computed on the fly when no run report exists (or when running via a CLI backend that doesnt generate the report).

View File

@@ -3,6 +3,7 @@ summary: "Behavior and config for WhatsApp group message handling (mentionPatter
read_when:
- Changing group message rules or mentions
---
# Group messages (WhatsApp web channel)
Goal: let Clawd sit in WhatsApp groups, wake up only when pinged, and keep that thread separate from the personal DM session.
@@ -10,15 +11,17 @@ Goal: let Clawd sit in WhatsApp groups, wake up only when pinged, and keep that
Note: `agents.list[].groupChat.mentionPatterns` is now used by Telegram/Discord/Slack/iMessage as well; this doc focuses on WhatsApp-specific behavior. For multi-agent setups, set `agents.list[].groupChat.mentionPatterns` per agent (or use `messages.groupChat.mentionPatterns` as a global fallback).
## Whats implemented (2025-12-03)
- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bots E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`channels.whatsapp.groups`) and overridden per group via `/activation`. When `channels.whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all).
- Group policy: `channels.whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `channels.whatsapp.groupAllowFrom` (fallback: explicit `channels.whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders).
- Per-group sessions: session keys look like `agent:<agentId>:whatsapp:group:<jid>` so commands such as `/verbose on` or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads.
- Context injection: **pending-only** group messages (default 50) that *did not* trigger a run are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`. Messages already in the session are not re-injected.
- Context injection: **pending-only** group messages (default 50) that _did not_ trigger a run are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`. Messages already in the session are not re-injected.
- Sender surfacing: every group batch now ends with `[from: Sender Name (+E164)]` so Pi knows who is speaking.
- Ephemeral/view-once: we unwrap those before extracting text/mentions, so pings inside them still trigger.
- Group system prompt: on the first turn of a group session (and whenever `/activation` changes the mode) we inject a short blurb into the system prompt like `You are replying inside the WhatsApp group "<subject>". Group members: Alice (+44...), Bob (+43...), … Activation: trigger-only … Address the specific sender noted in the message context.` If metadata isnt available we still tell the agent its a group chat.
## Config example (WhatsApp)
Add a `groupChat` block to `~/.openclaw/openclaw.json` so display-name pings work even when WhatsApp strips the visual `@` in the text body:
```json5
@@ -26,9 +29,9 @@ Add a `groupChat` block to `~/.openclaw/openclaw.json` so display-name pings wor
channels: {
whatsapp: {
groups: {
"*": { requireMention: true }
}
}
"*": { requireMention: true },
},
},
},
agents: {
list: [
@@ -36,42 +39,44 @@ Add a `groupChat` block to `~/.openclaw/openclaw.json` so display-name pings wor
id: "main",
groupChat: {
historyLimit: 50,
mentionPatterns: [
"@?openclaw",
"\\+?15555550123"
]
}
}
]
}
mentionPatterns: ["@?openclaw", "\\+?15555550123"],
},
},
],
},
}
```
Notes:
- The regexes are case-insensitive; they cover a display-name ping like `@openclaw` and the raw number with or without `+`/spaces.
- WhatsApp still sends canonical mentions via `mentionedJids` when someone taps the contact, so the number fallback is rarely needed but is a useful safety net.
### Activation command (owner-only)
Use the group chat command:
- `/activation mention`
- `/activation always`
Only the owner number (from `channels.whatsapp.allowFrom`, or the bots own E.164 when unset) can change this. Send `/status` as a standalone message in the group to see the current activation mode.
## How to use
1) Add your WhatsApp account (the one running OpenClaw) to the group.
2) Say `@openclaw …` (or include the number). Only allowlisted senders can trigger it unless you set `groupPolicy: "open"`.
3) The agent prompt will include recent group context plus the trailing `[from: …]` marker so it can address the right person.
4) Session-level directives (`/verbose on`, `/think high`, `/new` or `/reset`, `/compact`) apply only to that groups session; send them as standalone messages so they register. Your personal DM session remains independent.
1. Add your WhatsApp account (the one running OpenClaw) to the group.
2. Say `@openclaw …` (or include the number). Only allowlisted senders can trigger it unless you set `groupPolicy: "open"`.
3. The agent prompt will include recent group context plus the trailing `[from: …]` marker so it can address the right person.
4. Session-level directives (`/verbose on`, `/think high`, `/new` or `/reset`, `/compact`) apply only to that groups session; send them as standalone messages so they register. Your personal DM session remains independent.
## Testing / verification
- Manual smoke:
- Send an `@openclaw` ping in the group and confirm a reply that references the sender name.
- Send a second ping and verify the history block is included then cleared on the next turn.
- Check gateway logs (run with `--verbose`) to see `inbound web message` entries showing `from: <groupJid>` and the `[from: …]` suffix.
## Known considerations
- Heartbeats are intentionally skipped for groups to avoid noisy broadcasts.
- Echo suppression uses the combined batch string; if you send identical text twice without mentions, only the first will get a response.
- Session store entries will appear as `agent:<agentId>:whatsapp:group:<jid>` in the session store (`~/.openclaw/agents/<agentId>/sessions/sessions.json` by default); a missing entry just means the group hasnt triggered a run yet.

View File

@@ -3,26 +3,31 @@ summary: "Group chat behavior across surfaces (WhatsApp/Telegram/Discord/Slack/S
read_when:
- Changing group chat behavior or mention gating
---
# Groups
OpenClaw treats group chats consistently across surfaces: WhatsApp, Telegram, Discord, Slack, Signal, iMessage, Microsoft Teams.
## Beginner intro (2 minutes)
OpenClaw “lives” on your own messaging accounts. There is no separate WhatsApp bot user.
If **you** are in a group, OpenClaw can see that group and respond there.
Default behavior:
- Groups are restricted (`groupPolicy: "allowlist"`).
- Replies require a mention unless you explicitly disable mention gating.
Translation: allowlisted senders can trigger OpenClaw by mentioning it.
> TL;DR
>
> - **DM access** is controlled by `*.allowFrom`.
> - **Group access** is controlled by `*.groupPolicy` + allowlists (`*.groups`, `*.groupAllowFrom`).
> - **Reply triggering** is controlled by mention gating (`requireMention`, `/activation`).
Quick flow (what happens to a group message):
```
groupPolicy? disabled -> drop
groupPolicy? allowlist -> group allowed? no -> drop
@@ -41,6 +46,7 @@ If you want...
| Only you can trigger in groups | `groupPolicy: "allowlist"`, `groupAllowFrom: ["+1555..."]` |
## Session keys
- Group sessions use `agent:<agentId>:<channel>:group:<id>` session keys (rooms/channels use `agent:<agentId>:<channel>:channel:<id>`).
- Telegram forum topics add `:topic:<threadId>` to the group id so each topic has its own session.
- Direct chats use the main session (or per-sender if configured).
@@ -53,6 +59,7 @@ Yes — this works well if your “personal” traffic is **DMs** and your “pu
Why: in single-agent mode, DMs typically land in the **main** session key (`agent:main:main`), while groups always use **non-main** session keys (`agent:main:<channel>:group:<id>`). If you enable sandboxing with `mode: "non-main"`, those group sessions run in Docker while your main DM session stays on-host.
This gives you one agent “brain” (shared workspace + memory), but two execution postures:
- **DMs**: full tools (host)
- **Groups**: sandbox + restricted tools (Docker)
@@ -67,19 +74,19 @@ Example (DMs on host, groups sandboxed + messaging-only tools):
sandbox: {
mode: "non-main", // groups/channels are non-main -> sandboxed
scope: "session", // strongest isolation (one container per group/channel)
workspaceAccess: "none"
}
}
workspaceAccess: "none",
},
},
},
tools: {
sandbox: {
tools: {
// If allow is non-empty, everything else is blocked (deny still wins).
allow: ["group:messaging", "group:sessions"],
deny: ["group:runtime", "group:fs", "group:ui", "nodes", "cron", "gateway"]
}
}
}
deny: ["group:runtime", "group:fs", "group:ui", "nodes", "cron", "gateway"],
},
},
},
}
```
@@ -96,25 +103,28 @@ Want “groups can only see folder X” instead of “no host access”? Keep `w
docker: {
binds: [
// hostPath:containerPath:mode
"~/FriendsShared:/data:ro"
]
}
}
}
}
"~/FriendsShared:/data:ro",
],
},
},
},
},
}
```
Related:
- Configuration keys and defaults: [Gateway configuration](/gateway/configuration#agentsdefaultssandbox)
- Debugging why a tool is blocked: [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated)
- Bind mounts details: [Sandboxing](/gateway/sandboxing#custom-bind-mounts)
## Display labels
- UI labels use `displayName` when available, formatted as `<channel>:<token>`.
- `#room` is reserved for rooms/channels; group chats use `g-<slug>` (lowercase, spaces -> `-`, keep `#@+._-`).
## Group policy
Control how group/room messages are handled per channel:
```json5
@@ -122,53 +132,54 @@ Control how group/room messages are handled per channel:
channels: {
whatsapp: {
groupPolicy: "disabled", // "open" | "disabled" | "allowlist"
groupAllowFrom: ["+15551234567"]
groupAllowFrom: ["+15551234567"],
},
telegram: {
groupPolicy: "disabled",
groupAllowFrom: ["123456789", "@username"]
groupAllowFrom: ["123456789", "@username"],
},
signal: {
groupPolicy: "disabled",
groupAllowFrom: ["+15551234567"]
groupAllowFrom: ["+15551234567"],
},
imessage: {
groupPolicy: "disabled",
groupAllowFrom: ["chat_id:123"]
groupAllowFrom: ["chat_id:123"],
},
msteams: {
groupPolicy: "disabled",
groupAllowFrom: ["user@org.com"]
groupAllowFrom: ["user@org.com"],
},
discord: {
groupPolicy: "allowlist",
guilds: {
"GUILD_ID": { channels: { help: { allow: true } } }
}
GUILD_ID: { channels: { help: { allow: true } } },
},
},
slack: {
groupPolicy: "allowlist",
channels: { "#general": { allow: true } }
channels: { "#general": { allow: true } },
},
matrix: {
groupPolicy: "allowlist",
groupAllowFrom: ["@owner:example.org"],
groups: {
"!roomId:example.org": { allow: true },
"#alias:example.org": { allow: true }
}
}
}
"#alias:example.org": { allow: true },
},
},
},
}
```
| Policy | Behavior |
|--------|----------|
| `"open"` | Groups bypass allowlists; mention-gating still applies. |
| `"disabled"` | Block all group messages entirely. |
| Policy | Behavior |
| ------------- | ------------------------------------------------------------ |
| `"open"` | Groups bypass allowlists; mention-gating still applies. |
| `"disabled"` | Block all group messages entirely. |
| `"allowlist"` | Only allow groups/rooms that match the configured allowlist. |
Notes:
- `groupPolicy` is separate from mention-gating (which requires @mentions).
- WhatsApp/Telegram/Signal/iMessage/Microsoft Teams: use `groupAllowFrom` (fallback: explicit `allowFrom`).
- Discord: allowlist uses `channels.discord.guilds.<id>.channels`.
@@ -179,11 +190,13 @@ Notes:
- Default is `groupPolicy: "allowlist"`; if your group allowlist is empty, group messages are blocked.
Quick mental model (evaluation order for group messages):
1) `groupPolicy` (open/disabled/allowlist)
2) group allowlists (`*.groups`, `*.groupAllowFrom`, channel-specific allowlist)
3) mention gating (`requireMention`, `/activation`)
1. `groupPolicy` (open/disabled/allowlist)
2. group allowlists (`*.groups`, `*.groupAllowFrom`, channel-specific allowlist)
3. mention gating (`requireMention`, `/activation`)
## Mention gating (default)
Group messages require a mention unless overridden per group. Defaults live per subsystem under `*.groups."*"`.
Replying to a bot message counts as an implicit mention (when the channel supports reply metadata). This applies to Telegram, WhatsApp, Slack, Discord, and Microsoft Teams.
@@ -194,21 +207,21 @@ Replying to a bot message counts as an implicit mention (when the channel suppor
whatsapp: {
groups: {
"*": { requireMention: true },
"123@g.us": { requireMention: false }
}
"123@g.us": { requireMention: false },
},
},
telegram: {
groups: {
"*": { requireMention: true },
"123456789": { requireMention: false }
}
"123456789": { requireMention: false },
},
},
imessage: {
groups: {
"*": { requireMention: true },
"123": { requireMention: false }
}
}
"123": { requireMention: false },
},
},
},
agents: {
list: [
@@ -216,15 +229,16 @@ Replying to a bot message counts as an implicit mention (when the channel suppor
id: "main",
groupChat: {
mentionPatterns: ["@openclaw", "openclaw", "\\+15555550123"],
historyLimit: 50
}
}
]
}
historyLimit: 50,
},
},
],
},
}
```
Notes:
- `mentionPatterns` are case-insensitive regexes.
- Surfaces that provide explicit mentions still pass; patterns are a fallback.
- Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group).
@@ -233,16 +247,18 @@ Notes:
- Group history context is wrapped uniformly across channels and is **pending-only** (messages skipped due to mention gating); use `messages.groupChat.historyLimit` for the global default and `channels.<channel>.historyLimit` (or `channels.<channel>.accounts.*.historyLimit`) for overrides. Set `0` to disable.
## Group/channel tool restrictions (optional)
Some channel configs support restricting which tools are available **inside a specific group/room/channel**.
- `tools`: allow/deny tools for the whole group.
- `toolsBySender`: per-sender overrides within the group (keys are sender IDs/usernames/emails/phone numbers depending on the channel). Use `"*"` as a wildcard.
Resolution order (most specific wins):
1) group/channel `toolsBySender` match
2) group/channel `tools`
3) default (`"*"`) `toolsBySender` match
4) default (`"*"`) `tools`
1. group/channel `toolsBySender` match
2. group/channel `tools`
3. default (`"*"`) `toolsBySender` match
4. default (`"*"`) `tools`
Example (Telegram):
@@ -255,78 +271,88 @@ Example (Telegram):
"-1001234567890": {
tools: { deny: ["exec", "read", "write"] },
toolsBySender: {
"123456789": { alsoAllow: ["exec"] }
}
}
}
}
}
"123456789": { alsoAllow: ["exec"] },
},
},
},
},
},
}
```
Notes:
- Group/channel tool restrictions are applied in addition to global/agent tool policy (deny still wins).
- Some channels use different nesting for rooms/channels (e.g., Discord `guilds.*.channels.*`, Slack `channels.*`, MS Teams `teams.*.channels.*`).
## Group allowlists
When `channels.whatsapp.groups`, `channels.telegram.groups`, or `channels.imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior.
Common intents (copy/paste):
1) Disable all group replies
1. Disable all group replies
```json5
{
channels: { whatsapp: { groupPolicy: "disabled" } }
channels: { whatsapp: { groupPolicy: "disabled" } },
}
```
2) Allow only specific groups (WhatsApp)
2. Allow only specific groups (WhatsApp)
```json5
{
channels: {
whatsapp: {
groups: {
"123@g.us": { requireMention: true },
"456@g.us": { requireMention: false }
}
}
}
"456@g.us": { requireMention: false },
},
},
},
}
```
3) Allow all groups but require mention (explicit)
3. Allow all groups but require mention (explicit)
```json5
{
channels: {
whatsapp: {
groups: { "*": { requireMention: true } }
}
}
groups: { "*": { requireMention: true } },
},
},
}
```
4) Only the owner can trigger in groups (WhatsApp)
4. Only the owner can trigger in groups (WhatsApp)
```json5
{
channels: {
whatsapp: {
groupPolicy: "allowlist",
groupAllowFrom: ["+15551234567"],
groups: { "*": { requireMention: true } }
}
}
groups: { "*": { requireMention: true } },
},
},
}
```
## Activation (owner-only)
Group owners can toggle per-group activation:
- `/activation mention`
- `/activation always`
Owner is determined by `channels.whatsapp.allowFrom` (or the bots self E.164 when unset). Send the command as a standalone message. Other surfaces currently ignore `/activation`.
## Context fields
Group inbound payloads set:
- `ChatType=group`
- `GroupSubject` (if known)
- `GroupMembers` (if known)
@@ -336,9 +362,11 @@ Group inbound payloads set:
The agent system prompt includes a group intro on the first turn of a new group session. It reminds the model to respond like a human, avoid Markdown tables, and avoid typing literal `\n` sequences.
## iMessage specifics
- Prefer `chat_id:<id>` when routing or allowlisting.
- List chats: `imsg chats --limit 20`.
- Group replies always go back to the same `chat_id`.
## WhatsApp specifics
See [Group messages](/concepts/group-messages) for WhatsApp-only behavior (history injection, mention handling details).

View File

@@ -5,6 +5,7 @@ read_when:
- You are adding a new channel formatter or style mapping
- You are debugging formatting regressions across channels
---
# Markdown formatting
OpenClaw formats outbound Markdown by converting it into a shared intermediate
@@ -47,12 +48,8 @@ IR (schematic):
```json
{
"text": "Hello world — see docs.",
"styles": [
{ "start": 6, "end": 11, "style": "bold" }
],
"links": [
{ "start": 19, "end": 23, "href": "https://docs.openclaw.ai" }
]
"styles": [{ "start": 6, "end": 11, "style": "bold" }],
"links": [{ "start": 19, "end": 23, "href": "https://docs.openclaw.ai" }]
}
```

View File

@@ -4,6 +4,7 @@ read_when:
- You want the memory file layout and workflow
- You want to tune the automatic pre-compaction memory flush
---
# Memory
OpenClaw memory is **plain Markdown in the agent workspace**. The files are the
@@ -38,7 +39,7 @@ These files live under the workspace (`agents.defaults.workspace`, default
When a session is **close to auto-compaction**, OpenClaw triggers a **silent,
agentic turn** that reminds the model to write durable memory **before** the
context is compacted. The default prompts explicitly say the model *may reply*,
context is compacted. The default prompts explicitly say the model _may reply_,
but usually `NO_REPLY` is the correct response so the user never sees this turn.
This is controlled by `agents.defaults.compaction.memoryFlush`:
@@ -53,15 +54,16 @@ This is controlled by `agents.defaults.compaction.memoryFlush`:
enabled: true,
softThresholdTokens: 4000,
systemPrompt: "Session nearing compaction. Store durable memories now.",
prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store."
}
}
}
}
prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store.",
},
},
},
},
}
```
Details:
- **Soft threshold**: flush triggers when the session token estimate crosses
`contextWindow - reserveTokensFloor - softThresholdTokens`.
- **Silent** by default: prompts include `NO_REPLY` so nothing is delivered.
@@ -80,6 +82,7 @@ any extra directories or files you opt in) so semantic queries can find related
notes even when wording differs.
Defaults:
- Enabled by default.
- Watches memory files for changes (debounced).
- Uses remote embeddings by default. If `memorySearch.provider` is not set, OpenClaw auto-selects:
@@ -113,6 +116,7 @@ agents: {
```
Notes:
- Paths can be absolute or workspace-relative.
- Directories are scanned recursively for `.md` files.
- Only Markdown files are indexed.
@@ -137,6 +141,7 @@ agents: {
```
Notes:
- `remote.baseUrl` is optional (defaults to the Gemini API base URL).
- `remote.headers` lets you add extra headers if needed.
- Default model: `gemini-embedding-001`.
@@ -164,10 +169,12 @@ If you don't want to set an API key, use `memorySearch.provider = "local"` or se
`memorySearch.fallback = "none"`.
Fallbacks:
- `memorySearch.fallback` can be `openai`, `gemini`, `local`, or `none`.
- The fallback provider is only used when the primary embedding provider fails.
Batch indexing (OpenAI + Gemini):
- Enabled by default for OpenAI and Gemini embeddings. Set `agents.defaults.memorySearch.remote.batch.enabled = false` to disable.
- Default behavior waits for batch completion; tune `remote.batch.wait`, `remote.batch.pollIntervalMs`, and `remote.batch.timeoutMinutes` if needed.
- Set `remote.batch.concurrency` to control how many batch jobs we submit in parallel (default: 2).
@@ -175,6 +182,7 @@ Batch indexing (OpenAI + Gemini):
- Gemini batch jobs use the async embeddings batch endpoint and require Gemini Batch API availability.
Why OpenAI batch is fast + cheap:
- For large backfills, OpenAI is typically the fastest option we support because we can submit many embedding requests in a single batch job and let OpenAI process them asynchronously.
- OpenAI offers discounted pricing for Batch API workloads, so large indexing runs are usually cheaper than sending the same requests synchronously.
- See the OpenAI Batch API docs and pricing for details:
@@ -200,10 +208,12 @@ agents: {
```
Tools:
- `memory_search` — returns snippets with file + line ranges.
- `memory_get` — read memory file content by path.
Local mode:
- Set `agents.defaults.memorySearch.provider = "local"`.
- Provide `agents.defaults.memorySearch.local.modelPath` (GGUF or `hf:` URI).
- Optional: set `agents.defaults.memorySearch.fallback = "none"` to avoid remote fallback.
@@ -224,6 +234,7 @@ Local mode:
### Hybrid search (BM25 + vector)
When enabled, OpenClaw combines:
- **Vector similarity** (semantic match, wording can differ)
- **BM25 keyword relevance** (exact tokens like IDs, env vars, code symbols)
@@ -232,10 +243,12 @@ If full-text search is unavailable on your platform, OpenClaw falls back to vect
#### Why hybrid?
Vector search is great at “this means the same thing”:
- “Mac Studio gateway host” vs “the machine running the gateway”
- “debounce file updates” vs “avoid indexing on every write”
But it can be weak at exact, high-signal tokens:
- IDs (`a828e60`, `b3b9895a…`)
- code symbols (`memorySearch.query.hybrid`)
- error strings (“sqlite-vec unavailable”)
@@ -248,17 +261,21 @@ good results for both “natural language” queries and “needle in a haystack
Implementation sketch:
1) Retrieve a candidate pool from both sides:
1. Retrieve a candidate pool from both sides:
- **Vector**: top `maxResults * candidateMultiplier` by cosine similarity.
- **BM25**: top `maxResults * candidateMultiplier` by FTS5 BM25 rank (lower is better).
2) Convert BM25 rank into a 0..1-ish score:
2. Convert BM25 rank into a 0..1-ish score:
- `textScore = 1 / (1 + max(0, bm25Rank))`
3) Union candidates by chunk id and compute a weighted score:
3. Union candidates by chunk id and compute a weighted score:
- `finalScore = vectorWeight * vectorScore + textWeight * textScore`
Notes:
- `vectorWeight` + `textWeight` is normalized to 1.0 in config resolution, so weights behave as percentages.
- If embeddings are unavailable (or the provider returns a zero-vector), we still run BM25 and return keyword matches.
- If FTS5 cant be created, we keep vector-only search (no hard failure).
@@ -322,6 +339,7 @@ agents: {
```
Notes:
- Session indexing is **opt-in** (off by default).
- Session updates are debounced and **indexed asynchronously** once they cross delta thresholds (best-effort).
- `memory_search` never blocks on indexing; results can be slightly stale until background sync finishes.
@@ -370,6 +388,7 @@ agents: {
```
Notes:
- `enabled` defaults to true; when disabled, search falls back to in-process
cosine similarity over stored embeddings.
- If the sqlite-vec extension is missing or fails to load, OpenClaw logs the
@@ -406,5 +425,6 @@ agents: {
```
Notes:
- `remote.*` takes precedence over `models.providers.openai.*`.
- `remote.headers` merge with OpenAI headers; remote wins on key conflicts. Omit `remote.headers` to use the OpenAI defaults.

View File

@@ -5,6 +5,7 @@ read_when:
- Clarifying sessions, queueing modes, or streaming behavior
- Documenting reasoning visibility and usage implications
---
# Messages
This page ties together how OpenClaw handles inbound messages, sessions, queueing,
@@ -21,6 +22,7 @@ Inbound message
```
Key knobs live in configuration:
- `messages.*` for prefixes, queueing, and group behavior.
- `agents.defaults.*` for block streaming and chunking defaults.
- Channel overrides (`channels.whatsapp.*`, `channels.telegram.*`, etc.) for caps and streaming toggles.
@@ -40,6 +42,7 @@ agent turn via `messages.inbound`. Debouncing is scoped per channel + conversati
and uses the most recent message for reply threading/IDs.
Config (global default + per-channel overrides):
```json5
{
messages: {
@@ -48,20 +51,22 @@ Config (global default + per-channel overrides):
byChannel: {
whatsapp: 5000,
slack: 1500,
discord: 1500
}
}
}
discord: 1500,
},
},
},
}
```
Notes:
- Debounce applies to **text-only** messages; media/attachments flush immediately.
- Control commands bypass debouncing so they remain standalone.
## Sessions and devices
Sessions are owned by the gateway, not by clients.
- Direct chats collapse into the agent main session key.
- Groups/channels get their own session keys.
- The session store and transcripts live on the gateway host.
@@ -76,12 +81,14 @@ Details: [Session management](/concepts/session).
## Inbound bodies and history context
OpenClaw separates the **prompt body** from the **command body**:
- `Body`: prompt text sent to the agent. This may include channel envelopes and
optional history wrappers.
- `CommandBody`: raw user text for directive/command parsing.
- `RawBody`: legacy alias for `CommandBody` (kept for compatibility).
When a channel supplies history, it uses a shared wrapper:
- `[Chat messages since your last reply - for context]`
- `[Current message - respond to this]`
@@ -89,7 +96,7 @@ For **non-direct chats** (groups/channels/rooms), the **current message body** i
sender label (same style used for history entries). This keeps real-time and queued/history
messages consistent in the agent prompt.
History buffers are **pending-only**: they include group messages that did *not*
History buffers are **pending-only**: they include group messages that did _not_
trigger a run (for example, mention-gated messages) and **exclude** messages
already in the session transcript.
@@ -116,6 +123,7 @@ Block streaming sends partial replies as the model produces text blocks.
Chunking respects channel text limits and avoids splitting fenced code.
Key settings:
- `agents.defaults.blockStreamingDefault` (`on|off`, default off)
- `agents.defaults.blockStreamingBreak` (`text_end|message_end`)
- `agents.defaults.blockStreamingChunk` (`minChars|maxChars|breakPreference`)
@@ -128,6 +136,7 @@ Details: [Streaming + chunking](/concepts/streaming).
## Reasoning visibility and tokens
OpenClaw can expose or hide model reasoning:
- `/reasoning on|off|stream` controls visibility.
- Reasoning content still counts toward token usage when produced by the model.
- Telegram supports reasoning stream into the draft bubble.
@@ -137,6 +146,7 @@ Details: [Thinking + reasoning directives](/tools/thinking) and [Token use](/tok
## Prefixes, threading, and replies
Outbound message formatting is centralized in `messages`:
- `messages.responsePrefix` (outbound prefix) and `channels.whatsapp.messagePrefix` (WhatsApp inbound prefix)
- Reply threading via `replyToMode` and per-channel defaults

View File

@@ -8,8 +8,9 @@ read_when:
# Model failover
OpenClaw handles failures in two stages:
1) **Auth profile rotation** within the current provider.
2) **Model fallback** to the next model in `agents.defaults.model.fallbacks`.
1. **Auth profile rotation** within the current provider.
2. **Model fallback** to the next model in `agents.defaults.model.fallbacks`.
This doc explains the runtime rules and the data that backs them.
@@ -24,12 +25,14 @@ OpenClaw uses **auth profiles** for both API keys and OAuth tokens.
More detail: [/concepts/oauth](/concepts/oauth)
Credential types:
- `type: "api_key"``{ provider, key }`
- `type: "oauth"``{ provider, access, refresh, expires, email? }` (+ `projectId`/`enterpriseUrl` for some providers)
## Profile IDs
OAuth logins create distinct profiles so multiple accounts can coexist.
- Default: `provider:default` when no email is available.
- OAuth with email: `provider:<email>` (for example `google-antigravity:user@gmail.com`).
@@ -39,11 +42,12 @@ Profiles live in `~/.openclaw/agents/<agentId>/agent/auth-profiles.json` under `
When a provider has multiple profiles, OpenClaw chooses an order like this:
1) **Explicit config**: `auth.order[provider]` (if set).
2) **Configured profiles**: `auth.profiles` filtered by provider.
3) **Stored profiles**: entries in `auth-profiles.json` for the provider.
1. **Explicit config**: `auth.order[provider]` (if set).
2. **Configured profiles**: `auth.profiles` filtered by provider.
3. **Stored profiles**: entries in `auth-profiles.json` for the provider.
If no explicit order is configured, OpenClaw uses a roundrobin order:
- **Primary key:** profile type (**OAuth before API keys**).
- **Secondary key:** `usageStats.lastUsed` (oldest first, within each type).
- **Cooldown/disabled profiles** are moved to the end, ordered by soonest expiry.
@@ -52,6 +56,7 @@ If no explicit order is configured, OpenClaw uses a roundrobin order:
OpenClaw **pins the chosen auth profile per session** to keep provider caches warm.
It does **not** rotate on every request. The pinned profile is reused until:
- the session is reset (`/new` / `/reset`)
- a compaction completes (compaction count increments)
- the profile is in cooldown/disabled
@@ -67,6 +72,7 @@ are configured, OpenClaw moves to the next model instead of switching profiles.
### Why OAuth can “look lost”
If you have both an OAuth profile and an API key profile for the same provider, roundrobin can switch between them across messages unless pinned. To force a single profile:
- Pin with `auth.order[provider] = ["provider:profileId"]`, or
- Use a per-session override via `/model …` with a profile override (when supported by your UI/chat surface).
@@ -78,6 +84,7 @@ Format/invalidrequest errors (for example Cloud Code Assist tool call ID
validation failures) are treated as failoverworthy and use the same cooldowns.
Cooldowns use exponential backoff:
- 1 minute
- 5 minutes
- 25 minutes
@@ -115,6 +122,7 @@ State is stored in `auth-profiles.json`:
```
Defaults:
- Billing backoff starts at **5 hours**, doubles per billing failure, and caps at **24 hours**.
- Backoff counters reset if the profile hasnt failed for **24 hours** (configurable).
@@ -130,6 +138,7 @@ When a run starts with a model override (hooks or CLI), fallbacks still end at
## Related config
See [Gateway configuration](/gateway/configuration) for:
- `auth.profiles` / `auth.order`
- `auth.cooldowns.billingBackoffHours` / `auth.cooldowns.billingBackoffHoursByProvider`
- `auth.cooldowns.billingMaxHours` / `auth.cooldowns.failureWindowHours`

View File

@@ -4,6 +4,7 @@ read_when:
- You need a provider-by-provider model setup reference
- You want example configs or CLI onboarding commands for model providers
---
# Model providers
This page covers **LLM/model providers** (not chat channels like WhatsApp/Telegram).
@@ -29,7 +30,7 @@ OpenClaw ships with the piai catalog. These providers require **no**
```json5
{
agents: { defaults: { model: { primary: "openai/gpt-5.2" } } }
agents: { defaults: { model: { primary: "openai/gpt-5.2" } } },
}
```
@@ -42,7 +43,7 @@ OpenClaw ships with the piai catalog. These providers require **no**
```json5
{
agents: { defaults: { model: { primary: "anthropic/claude-opus-4-5" } } }
agents: { defaults: { model: { primary: "anthropic/claude-opus-4-5" } } },
}
```
@@ -55,7 +56,7 @@ OpenClaw ships with the piai catalog. These providers require **no**
```json5
{
agents: { defaults: { model: { primary: "openai-codex/gpt-5.2" } } }
agents: { defaults: { model: { primary: "openai-codex/gpt-5.2" } } },
}
```
@@ -68,7 +69,7 @@ OpenClaw ships with the piai catalog. These providers require **no**
```json5
{
agents: { defaults: { model: { primary: "opencode/claude-opus-4-5" } } }
agents: { defaults: { model: { primary: "opencode/claude-opus-4-5" } } },
}
```
@@ -132,17 +133,18 @@ Moonshot uses OpenAI-compatible endpoints, so configure it as a custom provider:
- Auth: `MOONSHOT_API_KEY`
- Example model: `moonshot/kimi-k2.5`
- Kimi K2 model IDs:
{/* moonshot-kimi-k2-model-refs:start */}
{/_ moonshot-kimi-k2-model-refs:start _/}
- `moonshot/kimi-k2.5`
- `moonshot/kimi-k2-0905-preview`
- `moonshot/kimi-k2-turbo-preview`
- `moonshot/kimi-k2-thinking`
- `moonshot/kimi-k2-thinking-turbo`
{/* moonshot-kimi-k2-model-refs:end */}
{/_ moonshot-kimi-k2-model-refs:end _/}
```json5
{
agents: {
defaults: { model: { primary: "moonshot/kimi-k2.5" } }
defaults: { model: { primary: "moonshot/kimi-k2.5" } },
},
models: {
mode: "merge",
@@ -151,10 +153,10 @@ Moonshot uses OpenAI-compatible endpoints, so configure it as a custom provider:
baseUrl: "https://api.moonshot.ai/v1",
apiKey: "${MOONSHOT_API_KEY}",
api: "openai-completions",
models: [{ id: "kimi-k2.5", name: "Kimi K2.5" }]
}
}
}
models: [{ id: "kimi-k2.5", name: "Kimi K2.5" }],
},
},
},
}
```
@@ -170,8 +172,8 @@ Kimi Coding uses Moonshot AI's Anthropic-compatible endpoint:
{
env: { KIMI_API_KEY: "sk-..." },
agents: {
defaults: { model: { primary: "kimi-coding/k2p5" } }
}
defaults: { model: { primary: "kimi-coding/k2p5" } },
},
}
```
@@ -186,6 +188,7 @@ openclaw models auth login --provider qwen-portal --set-default
```
Model refs:
- `qwen-portal/coder-model`
- `qwen-portal/vision-model`
@@ -203,7 +206,7 @@ Synthetic provides Anthropic-compatible models behind the `synthetic` provider:
```json5
{
agents: {
defaults: { model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M2.1" } }
defaults: { model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M2.1" } },
},
models: {
mode: "merge",
@@ -212,10 +215,10 @@ Synthetic provides Anthropic-compatible models behind the `synthetic` provider:
baseUrl: "https://api.synthetic.new/anthropic",
apiKey: "${SYNTHETIC_API_KEY}",
api: "anthropic-messages",
models: [{ id: "hf:MiniMaxAI/MiniMax-M2.1", name: "MiniMax M2.1" }]
}
}
}
models: [{ id: "hf:MiniMaxAI/MiniMax-M2.1", name: "MiniMax M2.1" }],
},
},
},
}
```
@@ -245,8 +248,8 @@ ollama pull llama3.3
```json5
{
agents: {
defaults: { model: { primary: "ollama/llama3.3" } }
}
defaults: { model: { primary: "ollama/llama3.3" } },
},
}
```
@@ -261,8 +264,8 @@ Example (OpenAIcompatible):
agents: {
defaults: {
model: { primary: "lmstudio/minimax-m2.1-gs32" },
models: { "lmstudio/minimax-m2.1-gs32": { alias: "Minimax" } }
}
models: { "lmstudio/minimax-m2.1-gs32": { alias: "Minimax" } },
},
},
models: {
providers: {
@@ -278,16 +281,17 @@ Example (OpenAIcompatible):
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 8192
}
]
}
}
}
maxTokens: 8192,
},
],
},
},
},
}
```
Notes:
- For custom providers, `reasoning`, `input`, `cost`, `contextWindow`, and `maxTokens` are optional.
When omitted, OpenClaw defaults to:
- `reasoning: false`

View File

@@ -5,6 +5,7 @@ read_when:
- Changing model fallback behavior or selection UX
- Updating model scan probes (tools/images)
---
# Models CLI
See [/concepts/model-failover](/concepts/model-failover) for auth profile
@@ -15,12 +16,13 @@ Quick provider overview + examples: [/concepts/model-providers](/concepts/model-
OpenClaw selects models in this order:
1) **Primary** model (`agents.defaults.model.primary` or `agents.defaults.model`).
2) **Fallbacks** in `agents.defaults.model.fallbacks` (in order).
3) **Provider auth failover** happens inside a provider before moving to the
1. **Primary** model (`agents.defaults.model.primary` or `agents.defaults.model`).
2. **Fallbacks** in `agents.defaults.model.fallbacks` (in order).
3. **Provider auth failover** happens inside a provider before moving to the
next model.
Related:
- `agents.defaults.models` is the allowlist/catalog of models OpenClaw can use (plus aliases).
- `agents.defaults.imageModel` is used **only when** the primary model cant accept images.
- Per-agent defaults can override `agents.defaults.model` via `agents.list[].model` plus bindings (see [/concepts/multi-agent](/concepts/multi-agent)).
@@ -80,9 +82,9 @@ Example allowlist config:
model: { primary: "anthropic/claude-sonnet-4-5" },
models: {
"anthropic/claude-sonnet-4-5": { alias: "Sonnet" },
"anthropic/claude-opus-4-5": { alias: "Opus" }
}
}
"anthropic/claude-opus-4-5": { alias: "Opus" },
},
},
}
```
@@ -99,6 +101,7 @@ You can switch models for the current session without restarting:
```
Notes:
- `/model` (and `/model list`) is a compact, numbered picker (model family + available providers).
- `/model <#>` selects from that picker.
- `/model status` is the detailed view (auth candidates and, when configured, provider endpoint `baseUrl` + `api` mode).
@@ -181,12 +184,14 @@ Probing requires an OpenRouter API key (from auth profiles or
`OPENROUTER_API_KEY`). Without a key, use `--no-probe` to list candidates only.
Scan results are ranked by:
1) Image support
2) Tool latency
3) Context size
4) Parameter count
1. Image support
2. Tool latency
3. Context size
4. Parameter count
Input
- OpenRouter `/models` list (filter `:free`)
- Requires OpenRouter API key from auth profiles or `OPENROUTER_API_KEY` (see [/environment](/environment))
- Optional filters: `--max-age-days`, `--min-params`, `--provider`, `--max-candidates`

View File

@@ -7,7 +7,7 @@ status: active
# Multi-Agent Routing
Goal: multiple *isolated* agents (separate workspace + `agentDir` + sessions), plus multiple channel accounts (e.g. two WhatsApps) in one running Gateway. Inbound is routed to an agent via bindings.
Goal: multiple _isolated_ agents (separate workspace + `agentDir` + sessions), plus multiple channel accounts (e.g. two WhatsApps) in one running Gateway. Inbound is routed to an agent via bindings.
## What is “one agent”?
@@ -93,23 +93,24 @@ Example:
agents: {
list: [
{ id: "alex", workspace: "~/.openclaw/workspace-alex" },
{ id: "mia", workspace: "~/.openclaw/workspace-mia" }
]
{ id: "mia", workspace: "~/.openclaw/workspace-mia" },
],
},
bindings: [
{ agentId: "alex", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551230001" } } },
{ agentId: "mia", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551230002" } } }
{ agentId: "mia", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551230002" } } },
],
channels: {
whatsapp: {
dmPolicy: "allowlist",
allowFrom: ["+15551230001", "+15551230002"]
}
}
allowFrom: ["+15551230001", "+15551230002"],
},
},
}
```
Notes:
- DM access control is **global per WhatsApp account** (pairing/allowlist), not per agent.
- For shared groups, bind the group to one agent or use [Broadcast groups](/broadcast-groups).
@@ -214,24 +215,25 @@ Split by channel: route WhatsApp to a fast everyday agent and Telegram to an Opu
id: "chat",
name: "Everyday",
workspace: "~/.openclaw/workspace-chat",
model: "anthropic/claude-sonnet-4-5"
model: "anthropic/claude-sonnet-4-5",
},
{
id: "opus",
name: "Deep Work",
workspace: "~/.openclaw/workspace-opus",
model: "anthropic/claude-opus-4-5"
}
]
model: "anthropic/claude-opus-4-5",
},
],
},
bindings: [
{ agentId: "chat", match: { channel: "whatsapp" } },
{ agentId: "opus", match: { channel: "telegram" } }
]
{ agentId: "opus", match: { channel: "telegram" } },
],
}
```
Notes:
- If you have multiple accounts for a channel, add `accountId` to the binding (for example `{ channel: "whatsapp", accountId: "personal" }`).
- To route a single DM/group to Opus while keeping the rest on chat, add a `match.peer` binding for that peer; peer matches always win over channel-wide rules.
@@ -243,14 +245,24 @@ Keep WhatsApp on the fast agent, but route one DM to Opus:
{
agents: {
list: [
{ id: "chat", name: "Everyday", workspace: "~/.openclaw/workspace-chat", model: "anthropic/claude-sonnet-4-5" },
{ id: "opus", name: "Deep Work", workspace: "~/.openclaw/workspace-opus", model: "anthropic/claude-opus-4-5" }
]
{
id: "chat",
name: "Everyday",
workspace: "~/.openclaw/workspace-chat",
model: "anthropic/claude-sonnet-4-5",
},
{
id: "opus",
name: "Deep Work",
workspace: "~/.openclaw/workspace-opus",
model: "anthropic/claude-opus-4-5",
},
],
},
bindings: [
{ agentId: "opus", match: { channel: "whatsapp", peer: { kind: "dm", id: "+15551234567" } } },
{ agentId: "chat", match: { channel: "whatsapp" } }
]
{ agentId: "chat", match: { channel: "whatsapp" } },
],
}
```
@@ -271,32 +283,41 @@ and a tighter tool policy:
workspace: "~/.openclaw/workspace-family",
identity: { name: "Family Bot" },
groupChat: {
mentionPatterns: ["@family", "@familybot", "@Family Bot"]
mentionPatterns: ["@family", "@familybot", "@Family Bot"],
},
sandbox: {
mode: "all",
scope: "agent"
scope: "agent",
},
tools: {
allow: ["exec", "read", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"],
deny: ["write", "edit", "apply_patch", "browser", "canvas", "nodes", "cron"]
}
}
]
allow: [
"exec",
"read",
"sessions_list",
"sessions_history",
"sessions_send",
"sessions_spawn",
"session_status",
],
deny: ["write", "edit", "apply_patch", "browser", "canvas", "nodes", "cron"],
},
},
],
},
bindings: [
{
agentId: "family",
match: {
channel: "whatsapp",
peer: { kind: "group", id: "120363999999999999@g.us" }
}
}
]
peer: { kind: "group", id: "120363999999999999@g.us" },
},
},
],
}
```
Notes:
- Tool allow/deny lists are **tools**, not skills. If a skill needs to run a
binary, ensure `exec` is allowed and the binary exists in the sandbox.
- For stricter gating, set `agents.list[].groupChat.mentionPatterns` and keep
@@ -343,6 +364,7 @@ Note: `setupCommand` lives under `sandbox.docker` and runs once on container cre
Per-agent `sandbox.docker.*` overrides are ignored when the resolved scope is `"shared"`.
**Benefits:**
- **Security isolation**: Restrict tools for untrusted agents
- **Resource control**: Sandbox specific agents while keeping others on host
- **Flexible policies**: Different permissions per agent

View File

@@ -6,6 +6,7 @@ read_when:
- You want setup-token or OAuth auth flows
- You want multiple accounts or profile routing
---
# OAuth
OpenClaw supports “subscription auth” via OAuth for providers that offer it (notably **OpenAI Codex (ChatGPT OAuth)**). For Anthropic subscriptions, use the **setup-token** flow. This page explains:
@@ -26,9 +27,11 @@ openclaw models auth login --provider <id>
OAuth providers commonly mint a **new refresh token** during login/refresh flows. Some providers (or OAuth clients) can invalidate older refresh tokens when a new one is issued for the same user/app.
Practical symptom:
- you log in via OpenClaw *and* via Claude Code / Codex CLI → one of them randomly gets “logged out” later
- you log in via OpenClaw _and_ via Claude Code / Codex CLI → one of them randomly gets “logged out” later
To reduce that, OpenClaw treats `auth-profiles.json` as a **token sink**:
- the runtime reads credentials from **one place**
- we can keep multiple profiles and route them deterministically
@@ -40,6 +43,7 @@ Secrets are stored **per-agent**:
- Runtime cache (managed automatically; dont edit): `~/.openclaw/agents/<agentId>/agent/auth.json`
Legacy import-only file (still supported, but not the main store):
- `~/.openclaw/credentials/oauth.json` (imported into `auth-profiles.json` on first use)
All of the above also respect `$OPENCLAW_STATE_DIR` (state dir override). Full reference: [/gateway/configuration](/gateway/configuration#auth-storage-oauth--api-keys)
@@ -72,9 +76,9 @@ OpenClaws interactive login flows are implemented in `@mariozechner/pi-ai` an
Flow shape:
1) run `claude setup-token`
2) paste the token into OpenClaw
3) store as a token auth profile (no refresh)
1. run `claude setup-token`
2. paste the token into OpenClaw
3. store as a token auth profile (no refresh)
The wizard path is `openclaw onboard` → auth choice `setup-token` (Anthropic).
@@ -82,12 +86,12 @@ The wizard path is `openclaw onboard` → auth choice `setup-token` (Anthropic).
Flow shape (PKCE):
1) generate PKCE verifier/challenge + random `state`
2) open `https://auth.openai.com/oauth/authorize?...`
3) try to capture callback on `http://127.0.0.1:1455/auth/callback`
4) if callback cant bind (or youre remote/headless), paste the redirect URL/code
5) exchange at `https://auth.openai.com/oauth/token`
6) extract `accountId` from the access token and store `{ access, refresh, expires, accountId }`
1. generate PKCE verifier/challenge + random `state`
2. open `https://auth.openai.com/oauth/authorize?...`
3. try to capture callback on `http://127.0.0.1:1455/auth/callback`
4. if callback cant bind (or youre remote/headless), paste the redirect URL/code
5. exchange at `https://auth.openai.com/oauth/token`
6. extract `accountId` from the access token and store `{ access, refresh, expires, accountId }`
Wizard path is `openclaw onboard` → auth choice `openai-codex`.
@@ -96,6 +100,7 @@ Wizard path is `openclaw onboard` → auth choice `openai-codex`.
Profiles store an `expires` timestamp.
At runtime:
- if `expires` is in the future → use the stored access token
- if expired → refresh (under a file lock) and overwrite the stored credentials
@@ -121,15 +126,19 @@ Then configure auth per-agent (wizard) and route chats to the right agent.
`auth-profiles.json` supports multiple profile IDs for the same provider.
Pick which profile is used:
- globally via config ordering (`auth.order`)
- per-session via `/model ...@<profileId>`
Example (session override):
- `/model Opus@anthropic:work`
How to see what profile IDs exist:
- `openclaw channels list --json` (shows `auth[]`)
Related docs:
- [/concepts/model-failover](/concepts/model-failover) (rotation + cooldown rules)
- [/tools/slash-commands](/tools/slash-commands) (command surface)

View File

@@ -5,9 +5,11 @@ read_when:
- Investigating duplicate or stale instance rows
- Changing gateway WS connect or system-event beacons
---
# Presence
OpenClaw “presence” is a lightweight, besteffort view of:
- the **Gateway** itself, and
- **clients connected to the Gateway** (mac app, WebChat, CLI, etc.)
@@ -53,6 +55,7 @@ Clients can send richer periodic beacons via the `system-event` method. The mac
app uses this to report host name, IP, and `lastInputSeconds`.
### 4) Node connects (role: node)
When a node connects over the Gateway WebSocket with `role: node`, the Gateway
upserts a presence entry for that node (same flow as other WS clients).

View File

@@ -3,15 +3,18 @@ summary: "Command queue design that serializes inbound auto-reply runs"
read_when:
- Changing auto-reply execution or concurrency
---
# Command Queue (2026-01-16)
We serialize inbound auto-reply runs (all channels) through a tiny in-process queue to prevent multiple agent runs from colliding, while still allowing safe parallelism across sessions.
## Why
- Auto-reply runs can be expensive (LLM calls) and can collide when multiple inbound messages arrive close together.
- Serializing avoids competing for shared resources (session files, logs, CLI stdin) and reduces the chance of upstream rate limits.
## How it works
- A lane-aware FIFO queue drains each lane with a configurable concurrency cap (default 1 for unconfigured lanes; main defaults to 4, subagent to 8).
- `runEmbeddedPiAgent` enqueues by **session key** (lane `session:<key>`) to guarantee only one active run per session.
- Each session run is then queued into a **global lane** (`main` by default) so overall parallelism is capped by `agents.defaults.maxConcurrent`.
@@ -19,7 +22,9 @@ We serialize inbound auto-reply runs (all channels) through a tiny in-process qu
- Typing indicators still fire immediately on enqueue (when supported by the channel) so user experience is unchanged while we wait our turn.
## Queue modes (per channel)
Inbound messages can steer the current run, wait for a followup turn, or do both:
- `steer`: inject immediately into the current run (cancels pending tool calls after the next tool boundary). If not streaming, falls back to followup.
- `followup`: enqueue for the next agent turn after the current run ends.
- `collect`: coalesce all queued messages into a **single** followup turn (default). If messages target different channels/threads, they drain individually to preserve routing.
@@ -33,6 +38,7 @@ one response per inbound message.
Send `/queue collect` as a standalone command (per-session) or set `messages.queue.byChannel.discord: "collect"`.
Defaults (when unset in config):
- All surfaces → `collect`
Configure globally or per channel via `messages.queue`:
@@ -45,14 +51,16 @@ Configure globally or per channel via `messages.queue`:
debounceMs: 1000,
cap: 20,
drop: "summarize",
byChannel: { discord: "collect" }
}
}
byChannel: { discord: "collect" },
},
},
}
```
## Queue options
Options apply to `followup`, `collect`, and `steer-backlog` (and to `steer` when it falls back to followup):
- `debounceMs`: wait for quiet before starting a followup turn (prevents “continue, continue”).
- `cap`: max queued messages per session.
- `drop`: overflow policy (`old`, `new`, `summarize`).
@@ -61,11 +69,13 @@ Summarize keeps a short bullet list of dropped messages and injects it as a synt
Defaults: `debounceMs: 1000`, `cap: 20`, `drop: summarize`.
## Per-session overrides
- Send `/queue <mode>` as a standalone command to store the mode for the current session.
- Options can be combined: `/queue collect debounce:2s cap:25 drop:summarize`
- `/queue default` or `/queue reset` clears the session override.
## Scope and guarantees
- Applies to auto-reply agent runs across all inbound channels that use the gateway reply pipeline (WhatsApp web, Telegram, Slack, Discord, Signal, iMessage, webchat, etc.).
- Default lane (`main`) is process-wide for inbound + main heartbeats; set `agents.defaults.maxConcurrent` to allow multiple sessions in parallel.
- Additional lanes may exist (e.g. `cron`, `subagent`) so background jobs can run in parallel without blocking inbound replies.
@@ -73,5 +83,6 @@ Defaults: `debounceMs: 1000`, `cap: 20`, `drop: summarize`.
- No external dependencies or background worker threads; pure TypeScript + promises.
## Troubleshooting
- If commands seem stuck, enable verbose logs and look for “queued for …ms” lines to confirm the queue is draining.
- If you need queue depth, enable verbose logs and watch for queue timing lines.

View File

@@ -4,14 +4,17 @@ read_when:
- Updating provider retry behavior or defaults
- Debugging provider send errors or rate limits
---
# Retry policy
## Goals
- Retry per HTTP request, not per multi-step flow.
- Preserve ordering by retrying only the current step.
- Avoid duplicating non-idempotent operations.
## Defaults
- Attempts: 3
- Max delay cap: 30000 ms
- Jitter: 0.1 (10 percent)
@@ -20,16 +23,20 @@ read_when:
- Discord min delay: 500 ms
## Behavior
### Discord
- Retries only on rate-limit errors (HTTP 429).
- Uses Discord `retry_after` when available, otherwise exponential backoff.
### Telegram
- Retries on transient errors (429, timeout, connect/reset/closed, temporarily unavailable).
- Uses `retry_after` when available, otherwise exponential backoff.
- Markdown parse errors are not retried; they fall back to plain text.
## Configuration
Set retry policy per provider in `~/.openclaw/openclaw.json`:
```json5
@@ -40,21 +47,22 @@ Set retry policy per provider in `~/.openclaw/openclaw.json`:
attempts: 3,
minDelayMs: 400,
maxDelayMs: 30000,
jitter: 0.1
}
jitter: 0.1,
},
},
discord: {
retry: {
attempts: 3,
minDelayMs: 500,
maxDelayMs: 30000,
jitter: 0.1
}
}
}
jitter: 0.1,
},
},
},
}
```
## Notes
- Retries apply per request (message send, media upload, reaction, poll, sticker).
- Composite flows do not retry completed steps.

View File

@@ -4,29 +4,34 @@ read_when:
- You want to reduce LLM context growth from tool outputs
- You are tuning agents.defaults.contextPruning
---
# 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`).
## When it runs
- 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 `cacheControlTtl`.
- After a prune, the TTL window resets so subsequent requests keep cache until `ttl` expires again.
- Only active for Anthropic API calls (and OpenRouter Anthropic models).
- For best results, match `ttl` to your model `cacheControlTtl`.
- After a prune, the TTL window resets so subsequent requests keep cache until `ttl` expires again.
## Smart defaults (Anthropic)
- **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 `cacheControlTtl` to `1h` on Anthropic models.
- If you set any of these values explicitly, OpenClaw does **not** override them.
## What this improves (cost + cache behavior)
- **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.
## What can be pruned
- Only `toolResult` messages.
- User + assistant messages are **never** modified.
- The last `keepLastAssistants` assistant messages are protected; tool results after that cutoff are not pruned.
@@ -34,34 +39,42 @@ Session pruning trims **old tool results** from the in-memory context right befo
- Tool results containing **image blocks** are skipped (never trimmed/cleared).
## Context window estimation
Pruning uses an estimated context window (chars ≈ tokens × 4). The window size is resolved in this order:
1) Model definition `contextWindow` (from the model registry).
2) `models.providers.*.models[].contextWindow` override.
3) `agents.defaults.contextTokens`.
4) Default `200000` tokens.
1. Model definition `contextWindow` (from the model registry).
2. `models.providers.*.models[].contextWindow` override.
3. `agents.defaults.contextTokens`.
4. Default `200000` tokens.
## Mode
### cache-ttl
- 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.
## Soft vs hard pruning
- **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`.
## Tool selection
- `tools.allow` / `tools.deny` support `*` wildcards.
- Deny wins.
- Matching is case-insensitive.
- Empty allow list => all tools allowed.
## Interaction with other limits
- 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`
@@ -71,33 +84,37 @@ Pruning uses an estimated context window (chars ≈ tokens × 4). The window siz
- `hardClear`: `{ enabled: true, placeholder: "[Old tool result content cleared]" }`
## Examples
Default (off):
```json5
{
agent: {
contextPruning: { mode: "off" }
}
contextPruning: { mode: "off" },
},
}
```
Enable TTL-aware pruning:
```json5
{
agent: {
contextPruning: { mode: "cache-ttl", ttl: "5m" }
}
contextPruning: { mode: "cache-ttl", ttl: "5m" },
},
}
```
Restrict pruning to specific tools:
```json5
{
agent: {
contextPruning: {
mode: "cache-ttl",
tools: { allow: ["exec", "read"], deny: ["*image*"] }
}
}
tools: { allow: ["exec", "read"], deny: ["*image*"] },
},
},
}
```

View File

@@ -9,12 +9,14 @@ read_when:
Goal: small, hard-to-misuse tool set so agents can list sessions, fetch history, and send to another session.
## Tool Names
- `sessions_list`
- `sessions_history`
- `sessions_send`
- `sessions_spawn`
## Key Model
- 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>`.
@@ -24,20 +26,24 @@ Goal: small, hard-to-misuse tool set so agents can list sessions, fetch history,
`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`.
## sessions_list
List sessions as an array of rows.
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
Behavior:
- `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`
@@ -53,27 +59,33 @@ Row shape (JSON):
- `messages?` (only when `messageLimit > 0`)
## sessions_history
Fetch transcript for one session.
Parameters:
- `sessionKey` (required; accepts session key or `sessionId` from `sessions_list`)
- `limit?: number` max messages (server clamps)
- `includeTools?: boolean` (default false)
Behavior:
- `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).
## sessions_send
Send a message into another session.
Parameters:
- `sessionKey` (required; accepts session key or `sessionId` from `sessions_list`)
- `message` (required)
- `timeoutSeconds?: number` (default >0; 0 = fire-and-forget)
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.
@@ -91,12 +103,14 @@ Behavior:
- Announce step includes the original request + round1 reply + latest pingpong reply.
## Channel Field
- 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`.
## Security / Send Policy
Policy-based blocking by channel/chat type (not per session id).
```json
@@ -116,17 +130,21 @@ Policy-based blocking by channel/chat type (not per session id).
```
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
## sessions_spawn
Spawn a sub-agent run in an isolated session and announce the result back to the requester chat channel.
Parameters:
- `task` (required)
- `label?` (optional; used for logs/UI)
- `agentId?` (optional; spawn under another agent id if allowed)
@@ -135,12 +153,15 @@ Parameters:
- `cleanup?` (`delete|keep`, default `keep`)
Allowlist:
- `agents.list[].subagents.allowAgents`: list of agent ids allowed via `agentId` (`["*"]` to allow any). Default: only the requester agent.
Discovery:
- Use `agents_list` to discover which agent ids are allowed for `sessions_spawn`.
Behavior:
- 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).
@@ -163,9 +184,9 @@ Config:
defaults: {
sandbox: {
// default: "spawned"
sessionToolsVisibility: "spawned" // or "all"
}
}
}
sessionToolsVisibility: "spawned", // or "all"
},
},
},
}
```

View File

@@ -3,24 +3,28 @@ summary: "Session management rules, keys, and persistence for chats"
read_when:
- Modifying session handling or storage
---
# 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.
Use `session.dmScope` to control how **direct messages** are grouped:
- `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`.
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`.
## Gateway is the source of truth
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.
- 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.
## 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`).
@@ -30,16 +34,19 @@ All session state is **owned by the gateway** (the “master” OpenClaw). UI cl
- OpenClaw does **not** read legacy Pi/Tau session folders.
## Session pruning
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).
## Pre-compaction memory flush
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).
## 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.
@@ -57,6 +64,7 @@ the workspace is writable. See [Memory](/concepts/memory) and
- 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.
@@ -68,6 +76,7 @@ the workspace is writable. See [Memory](/concepts/memory) and
- 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.
```json5
@@ -76,53 +85,56 @@ Block delivery for specific session types without listing individual ids.
sendPolicy: {
rules: [
{ action: "deny", match: { channel: "discord", chatType: "group" } },
{ action: "deny", match: { keyPrefix: "cron:" } }
{ action: "deny", match: { keyPrefix: "cron:" } },
],
default: "allow"
}
}
default: "allow",
},
},
}
```
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 these as standalone messages so they register.
## Configuration (optional rename example)
```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)
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"]
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
idleMinutes: 120,
},
resetByType: {
thread: { mode: "daily", atHour: 4 },
dm: { mode: "idle", idleMinutes: 240 },
group: { mode: "idle", idleMinutes: 120 }
group: { mode: "idle", idleMinutes: 120 },
},
resetByChannel: {
discord: { mode: "idle", idleMinutes: 10080 }
discord: { mode: "idle", idleMinutes: 10080 },
},
resetTriggers: ["/new", "/reset"],
store: "~/.openclaw/agents/{agentId}/sessions/sessions.json",
mainKey: "main",
}
},
}
```
## 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).
@@ -133,20 +145,23 @@ Send these as standalone messages so they register.
- 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.
## 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`).
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`).

View File

@@ -3,6 +3,7 @@ summary: "Alias for session management docs"
read_when:
- You looked for docs/sessions.md; canonical doc lives in docs/session.md
---
# Sessions
Canonical session management docs live in [Session management](/concepts/session).

View File

@@ -5,9 +5,11 @@ read_when:
- Changing block streaming or channel chunking behavior
- Debugging duplicate/early block replies or draft streaming
---
# Streaming + chunking
OpenClaw has two separate “streaming” layers:
- **Block streaming (channels):** emit completed **blocks** as the assistant writes. These are normal channel messages (not token deltas).
- **Token-ish streaming (Telegram only):** update a **draft bubble** with partial text while generating; final message is sent at the end.
@@ -26,12 +28,15 @@ Model output
└─ chunker flushes at message_end
└─ channel send (block replies)
```
Legend:
- `text_delta/events`: model stream events (may be sparse for non-streaming models).
- `chunker`: `EmbeddedBlockChunker` applying min/max bounds + break preference.
- `channel send`: actual outbound messages (block replies).
**Controls:**
- `agents.defaults.blockStreamingDefault`: `"on"`/`"off"` (default off).
- Channel overrides: `*.blockStreaming` (and per-account variants) to force `"on"`/`"off"` per channel.
- `agents.defaults.blockStreamingBreak`: `"text_end"` or `"message_end"`.
@@ -42,6 +47,7 @@ Legend:
- Discord soft cap: `channels.discord.maxLinesPerMessage` (default 17) splits tall replies to avoid UI clipping.
**Boundary semantics:**
- `text_end`: stream blocks as soon as chunker emits; flush on each `text_end`.
- `message_end`: wait until assistant message finishes, then flush buffered output.
@@ -50,6 +56,7 @@ Legend:
## Chunking algorithm (low/high bounds)
Block chunking is implemented by `EmbeddedBlockChunker`:
- **Low bound:** dont emit until buffer >= `minChars` (unless forced).
- **High bound:** prefer splits before `maxChars`; if forced, split at `maxChars`.
- **Break preference:** `paragraph``newline``sentence``whitespace` → hard break.
@@ -85,6 +92,7 @@ more natural.
## “Stream chunks or everything”
This maps to:
- **Stream chunks:** `blockStreamingDefault: "on"` + `blockStreamingBreak: "text_end"` (emit as you go). Non-Telegram channels also need `*.blockStreaming: true`.
- **Stream everything at end:** `blockStreamingBreak: "message_end"` (flush once, possibly multiple chunks if very long).
- **No block streaming:** `blockStreamingDefault: "off"` (only final reply).
@@ -99,6 +107,7 @@ Config location reminder: the `blockStreaming*` defaults live under
## Telegram draft streaming (token-ish)
Telegram is the only channel with draft streaming:
- Uses Bot API `sendMessageDraft` in **private chats with topics**.
- `channels.telegram.streamMode: "partial" | "block" | "off"`.
- `partial`: draft updates with the latest stream text.
@@ -118,6 +127,8 @@ Telegram (private + topics)
└─ streamMode=block → chunker updates draft
└─ final reply → normal message
```
Legend:
- `sendMessageDraft`: Telegram draft bubble (not a real message).
- `final reply`: normal Telegram message send.

View File

@@ -4,6 +4,7 @@ read_when:
- Editing system prompt text, tools list, or time/heartbeat sections
- Changing workspace bootstrap or skills injection behavior
---
# System Prompt
OpenClaw builds a custom system prompt for every agent run. The prompt is **OpenClaw-owned** and does not use the p-coding-agent default prompt.

View File

@@ -27,9 +27,9 @@ You can override this with:
defaults: {
envelopeTimezone: "local", // "utc" | "local" | "user" | IANA timezone
envelopeTimestamp: "on", // "on" | "off"
envelopeElapsed: "on" // "on" | "off"
}
}
envelopeElapsed: "on", // "on" | "off"
},
},
}
```
@@ -76,11 +76,12 @@ unset, OpenClaw resolves the **host timezone at runtime** (no config write).
```json5
{
agents: { defaults: { userTimezone: "America/Chicago" } }
agents: { defaults: { userTimezone: "America/Chicago" } },
}
```
The system prompt includes:
- `Current Date & Time` section with local time and timezone
- `Time format: 12-hour` or `24-hour`

View File

@@ -3,6 +3,7 @@ summary: "TypeBox schemas as the single source of truth for the gateway protocol
read_when:
- Updating protocol schemas or codegen
---
# TypeBox as protocol source of truth
Last updated: 2026-01-10
@@ -40,14 +41,14 @@ Client Gateway
Common methods + events:
| Category | Examples | Notes |
| --- | --- | --- |
| Core | `connect`, `health`, `status` | `connect` must be first |
| Messaging | `send`, `poll`, `agent`, `agent.wait` | side-effects need `idempotencyKey` |
| Chat | `chat.history`, `chat.send`, `chat.abort`, `chat.inject` | WebChat uses these |
| Sessions | `sessions.list`, `sessions.patch`, `sessions.delete` | session admin |
| Nodes | `node.list`, `node.invoke`, `node.pair.*` | Gateway WS + node actions |
| Events | `tick`, `presence`, `agent`, `chat`, `health`, `shutdown` | server push |
| Category | Examples | Notes |
| --------- | --------------------------------------------------------- | ---------------------------------- |
| Core | `connect`, `health`, `status` | `connect` must be first |
| Messaging | `send`, `poll`, `agent`, `agent.wait` | side-effects need `idempotencyKey` |
| Chat | `chat.history`, `chat.send`, `chat.abort`, `chat.inject` | WebChat uses these |
| Sessions | `sessions.list`, `sessions.patch`, `sessions.delete` | session admin |
| Nodes | `node.list`, `node.invoke`, `node.pair.*` | Gateway WS + node actions |
| Events | `tick`, `presence`, `agent`, `chat`, `health`, `shutdown` | server push |
Authoritative list lives in `src/gateway/server.ts` (`METHODS`, `EVENTS`).
@@ -114,7 +115,12 @@ Hello-ok response:
"protocol": 2,
"server": { "version": "dev", "connId": "ws-1" },
"features": { "methods": ["health"], "events": ["tick"] },
"snapshot": { "presence": [], "health": {}, "stateVersion": { "presence": 0, "health": 0 }, "uptimeMs": 0 },
"snapshot": {
"presence": [],
"health": {},
"stateVersion": { "presence": 0, "health": 0 },
"uptimeMs": 0
},
"policy": { "maxPayload": 1048576, "maxBufferedBytes": 1048576, "tickIntervalMs": 30000 }
}
}
@@ -146,22 +152,24 @@ import { WebSocket } from "ws";
const ws = new WebSocket("ws://127.0.0.1:18789");
ws.on("open", () => {
ws.send(JSON.stringify({
type: "req",
id: "c1",
method: "connect",
params: {
minProtocol: 3,
maxProtocol: 3,
client: {
id: "cli",
displayName: "example",
version: "dev",
platform: "node",
mode: "cli"
}
}
}));
ws.send(
JSON.stringify({
type: "req",
id: "c1",
method: "connect",
params: {
minProtocol: 3,
maxProtocol: 3,
client: {
id: "cli",
displayName: "example",
version: "dev",
platform: "node",
mode: "cli",
},
},
}),
);
});
ws.on("message", (data) => {
@@ -180,7 +188,7 @@ ws.on("message", (data) => {
Example: add a new `system.echo` request that returns `{ ok: true, text }`.
1) **Schema (source of truth)**
1. **Schema (source of truth)**
Add to `src/gateway/protocol/schema.ts`:
@@ -208,16 +216,15 @@ export type SystemEchoParams = Static<typeof SystemEchoParamsSchema>;
export type SystemEchoResult = Static<typeof SystemEchoResultSchema>;
```
2) **Validation**
2. **Validation**
In `src/gateway/protocol/index.ts`, export an AJV validator:
```ts
export const validateSystemEchoParams =
ajv.compile<SystemEchoParams>(SystemEchoParamsSchema);
export const validateSystemEchoParams = ajv.compile<SystemEchoParams>(SystemEchoParamsSchema);
```
3) **Server behavior**
3. **Server behavior**
Add a handler in `src/gateway/server-methods/system.ts`:
@@ -233,13 +240,13 @@ export const systemHandlers: GatewayRequestHandlers = {
Register it in `src/gateway/server-methods.ts` (already merges `systemHandlers`),
then add `"system.echo"` to `METHODS` in `src/gateway/server.ts`.
4) **Regenerate**
4. **Regenerate**
```bash
pnpm protocol:check
```
5) **Tests + docs**
5. **Tests + docs**
Add a server test in `src/gateway/server.*.test.ts` and note the method in docs.
@@ -276,6 +283,6 @@ published raw file is typically available at:
## When you change schemas
1) Update the TypeBox schemas.
2) Run `pnpm protocol:check`.
3) Commit the regenerated schema + Swift models.
1. Update the TypeBox schemas.
2. Run `pnpm protocol:check`.
3. Commit the regenerated schema + Swift models.

View File

@@ -3,6 +3,7 @@ summary: "When OpenClaw shows typing indicators and how to tune them"
read_when:
- Changing typing indicator behavior or defaults
---
# Typing indicators
Typing indicators are sent to the chat channel while a run is active. Use
@@ -10,14 +11,18 @@ Typing indicators are sent to the chat channel while a run is active. Use
to control **how often** it refreshes.
## Defaults
When `agents.defaults.typingMode` is **unset**, OpenClaw keeps the legacy behavior:
- **Direct chats**: typing starts immediately once the model loop begins.
- **Group chats with a mention**: typing starts immediately.
- **Group chats without a mention**: typing starts only when message text begins streaming.
- **Heartbeat runs**: typing is disabled.
## Modes
Set `agents.defaults.typingMode` to one of:
- `never` — no typing indicator, ever.
- `instant` — start typing **as soon as the model loop begins**, even if the run
later returns only the silent reply token.
@@ -30,26 +35,29 @@ Order of “how early it fires”:
`never``message``thinking``instant`
## Configuration
```json5
{
agent: {
typingMode: "thinking",
typingIntervalSeconds: 6
}
typingIntervalSeconds: 6,
},
}
```
You can override mode or cadence per session:
```json5
{
session: {
typingMode: "message",
typingIntervalSeconds: 4
}
typingIntervalSeconds: 4,
},
}
```
## Notes
- `message` mode wont show typing for silent-only replies (e.g. the `NO_REPLY`
token used to suppress output).
- `thinking` only fires if the run streams reasoning (`reasoningLevel: "stream"`).

View File

@@ -4,13 +4,16 @@ read_when:
- You are wiring provider usage/quota surfaces
- You need to explain usage tracking behavior or auth requirements
---
# Usage tracking
## What it is
- Pulls provider usage/quota directly from their usage endpoints.
- No estimated costs; only the provider-reported windows.
## Where it shows up
- `/status` in chats: emojirich status card with session tokens + estimated cost (API key only). Provider usage shows for the **current model provider** when available.
- `/usage off|tokens|full` in chats: per-response usage footer (OAuth shows tokens only).
- `/usage cost` in chats: local cost summary aggregated from OpenClaw session logs.
@@ -19,6 +22,7 @@ read_when:
- macOS menu bar: “Usage” section under Context (only if available).
## Providers + credentials
- **Anthropic (Claude)**: OAuth tokens in auth profiles.
- **GitHub Copilot**: OAuth tokens in auth profiles.
- **Gemini CLI**: OAuth tokens in auth profiles.