mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-17 14:51:35 +00:00
* refactor(sessions): migrate runtime storage to sqlite * test(sessions): fix sqlite CI regressions * test(sessions): align remaining sqlite fixtures * fix(codex): require sqlite trajectory recorder * test(sessions): align orphan recovery sqlite fixture * test(sessions): align sqlite rebase fixtures * fix(sessions): finish current-main integration of the sqlite flip Resolve the whole-store SDK removal across its owner boundary: drop the loadSessionStore re-export and the registry whole-store wrappers, wire hasTrackedActiveSessionRun into gateway chat, complete the preserveLockedHarnessIds cleanup contract, flip the codex thread-history import to storePath targets, and port remaining main-side tests from file-store helpers to session accessor reads. * chore: drop committed pebbles log, revert plugin-inspector bump, refresh generated docs Remove the 1.8k-line .pebbles/events.jsonl work log from the branch, restore the plugin-inspector advisory lane to main's pinned 0.3.10 so the supply-chain bump gets its own review, and regenerate docs_map, the plugin SDK API baseline, and the export-surface ratchet for the merged tree. * feat(sessions): keep archived transcripts by default with zstd cold storage Codex-style retention: deleting or resetting a session archives its transcript as a zstd-compressed JSONL artifact (plain when the runtime lacks node:zlib zstd) and keeps it until the disk budget evicts oldest first. resetArchiveRetention now governs both deleted and reset archives and defaults to keep; maxDiskBytes defaults to 2gb so retention stays bounded, with archives evicted before live sessions. The cron reaper follows the same knob instead of deleting archives on its own timer. * fix(state): converge agent DB migration lineages and bound database growth Merge coherence: run both structure-gated legacy memory-schema repairs (flip-lineage drop, main-lineage identity rebuild) before the flip migration so pre-flip v1/v2 and pre-merge flip v1/v4 databases all converge, and hoist foreign_keys=OFF outside the schema transaction where the pragma was silently ignored and the v1 sessions rebuild cascade-deleted session_entries. Growth guards: fresh agent DBs enable auto_vacuum=INCREMENTAL, WAL maintenance releases freed pages in bounded passes (never a blocking full VACUUM), and doctor reports state/agent DB bloat from freelist stats. * fix(codex): resolve the store path for thread-history import via the SDK The supervision catalog passed the legacy sessionFile locator to the storePath-targeted transcript mirror; resolve the agent store path with the session-store SDK helper instead of a runtime-object seam so test fakes and headless callers need no extra surface. Drop the obsolete missing-session-id preprocessing case: sessions rows are NOT NULL on session_id and upsert repairs id-less patches at write time. * fix(sessions): fail safe on malformed disk-budget config and doctor stat errors A malformed explicit maxDiskBytes disables the budget instead of falling back to the destructive 2gb default the user never chose, and the doctor bloat check skips databases whose paths stat-fail instead of aborting doctor. * fix(sessions): complete sqlite conflict translations * test(sqlite): align hardening checks with maintenance * test(sessions): inspect compressed transcript archives * fix(tests): await session seeds and drop unused helpers flagged by CI lint The five unawaited writeSessionStoreSeed calls raced their SQLite seeds against the assertions, failing compact shards; the bloat probe drops a useless initializer and the merged tests drop now-unused helpers. * test(sessions): type legacy proof events directly * test(sessions): align hardening contracts * perf(sessions): read usage transcript sizes from SQL aggregates Usage/cost scans walked every session and materialized every transcript event just to re-stringify it for a byte estimate — the #86718 stall class reborn on the DB. readTranscriptStatsSync sums stored JSON bytes in SQLite without loading a single row. * fix(sessions): re-root foreign-root transcript paths onto the current sessions dir Restored backups, moved OPENCLAW_STATE_DIR, and rehearsal copies carry absolute sessionFile paths from the old root; the containment fallback kept those foreign paths, so migration read (and would archive) files in the original root and reported local copies missing. Re-root the canonical agents/<id>/sessions suffix onto the current dir when the file exists there; genuine cross-root layouts still fall through unchanged. * test(agents): seed harness admission through sqlite * fix(sqlite): close agent db on pragma setup failure * fix(doctor): compact and retrofit incremental auto-vacuum after session import The migration is the sanctioned offline window: post-import compact reclaims import churn and applies auto_vacuum=INCREMENTAL to databases created before the fresh-DB pragma existed, so runtime maintenance can release pages in bounded passes on every install. --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
177 lines
7.2 KiB
Markdown
177 lines
7.2 KiB
Markdown
---
|
|
summary: "Routing rules per channel (WhatsApp, Telegram, Discord, Slack) and shared context"
|
|
read_when:
|
|
- Changing channel routing or inbox behavior
|
|
title: "Channel 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
|
|
host configuration.
|
|
|
|
## Key terms
|
|
|
|
- **Channel**: a bundled channel plugin such as `discord`, `googlechat`, `imessage`, `irc`, `line`, `signal`, `slack`, `telegram`, or `whatsapp`, plus installed plugin channels. `webchat` is the internal WebChat UI channel and is not a configurable outbound channel.
|
|
- **AccountId**: per-channel account instance (when supported).
|
|
- Optional channel default account: `channels.<channel>.defaultAccount` chooses
|
|
which account is used when an outbound path does not specify `accountId`.
|
|
- In multi-account setups, set an explicit default (`defaultAccount` or an account named `default`) when two or more accounts are configured. Without it, fallback routing may pick the first normalized account ID.
|
|
- **AgentId**: an isolated workspace + session store ("brain").
|
|
- **SessionKey**: the bucket key used to store context and control concurrency.
|
|
|
|
## Outbound target prefixes
|
|
|
|
Explicit outbound targets may include a provider prefix, such as `telegram:123` or `tg:123`. Core treats that prefix as a channel-selection hint only when the selected channel is `last` or otherwise unresolved, and only when the loaded plugin advertises that prefix. If the caller already selected an explicit channel, the provider prefix must match that channel; cross-channel combinations such as WhatsApp delivery to `telegram:123` fail before plugin-specific target normalization.
|
|
|
|
Target-kind and service prefixes such as `channel:<id>`, `user:<id>`, `room:<id>`, `thread:<id>`, `imessage:<handle>`, and `sms:<number>` stay inside the selected channel's grammar. They do not select the provider by themselves.
|
|
|
|
## Session key shapes (examples)
|
|
|
|
Direct messages collapse to the agent's **main** session by default:
|
|
|
|
- `agent:<agentId>:<mainKey>` (default: `agent:main:main`)
|
|
|
|
`session.dmScope` controls DM collapsing: `main` (default) shares one main
|
|
session, while `per-peer`, `per-channel-peer`, and `per-account-channel-peer`
|
|
keep DMs in separate sessions. A route binding can override the scope for its
|
|
matched peers via `bindings[].session.dmScope`.
|
|
|
|
Even when direct-message conversation history is shared with main, sandbox and
|
|
tool policy use a derived per-account direct-chat runtime key for external DMs
|
|
so channel-originated messages are not treated like local main-session runs.
|
|
|
|
Groups and channels remain isolated per channel:
|
|
|
|
- Groups: `agent:<agentId>:<channel>:group:<id>`
|
|
- Channels/rooms: `agent:<agentId>:<channel>:channel:<id>`
|
|
|
|
Threads:
|
|
|
|
- Slack/Discord threads append `:thread:<threadId>` to the base key.
|
|
- Telegram forum topics embed `:topic:<topicId>` in the group key.
|
|
|
|
Examples:
|
|
|
|
- `agent:main:telegram:group:-1001234567890:topic:42`
|
|
- `agent:main:discord:channel:123456:thread:987654`
|
|
|
|
## Main DM route pinning
|
|
|
|
When `session.dmScope` is `main`, direct messages may share one main session.
|
|
To prevent the session's `lastRoute` from being overwritten by non-owner DMs,
|
|
OpenClaw infers a pinned owner from `allowFrom` when all of these are true:
|
|
|
|
- `allowFrom` has exactly one non-wildcard entry.
|
|
- The entry can be normalized to a concrete sender ID for that channel.
|
|
- The inbound DM sender does not match that pinned owner.
|
|
|
|
In that mismatch case, OpenClaw still records inbound session metadata, but it
|
|
skips updating the main session `lastRoute`.
|
|
|
|
## Guarded inbound recording
|
|
|
|
Channel plugins can mark an inbound session record as `createIfMissing: false`
|
|
when a guarded path must not create a new OpenClaw session. In that mode,
|
|
OpenClaw may update metadata and `lastRoute` for an existing session, but it
|
|
does not create a route-only session entry just because a message was observed.
|
|
|
|
## Routing rules (how an agent is chosen)
|
|
|
|
Routing picks **one agent** for each inbound message:
|
|
|
|
1. **Exact peer match** (`bindings` with `peer.kind` + `peer.id`).
|
|
2. **Parent peer match** (thread inheritance).
|
|
3. **Peer wildcard match** (`peer.id: "*"` for a peer kind).
|
|
4. **Guild + roles match** (Discord) via `guildId` + `roles`.
|
|
5. **Guild match** (Discord) via `guildId`.
|
|
6. **Team match** (Slack) via `teamId`.
|
|
7. **Account match** (`accountId` on the channel).
|
|
8. **Channel match** (any account on that channel, `accountId: "*"`).
|
|
9. **Default agent** (`agents.list[].default`, else first list entry, fallback to `main`).
|
|
|
|
When a binding includes multiple match fields (`peer`, `guildId`, `teamId`, `roles`), **all provided fields must match** for that binding to apply.
|
|
|
|
The matched agent determines which workspace and session store are used.
|
|
|
|
## Broadcast groups (run multiple agents)
|
|
|
|
Broadcast groups let you run **multiple agents** for the same peer **when OpenClaw would normally reply** (for example: in WhatsApp groups, after mention/activation gating).
|
|
|
|
Config:
|
|
|
|
```json5
|
|
{
|
|
broadcast: {
|
|
strategy: "parallel",
|
|
"120363403215116621@g.us": ["alfred", "baerbel"],
|
|
"+15555550123": ["support", "logger"],
|
|
},
|
|
}
|
|
```
|
|
|
|
See: [Broadcast Groups](/channels/broadcast-groups).
|
|
|
|
## Config overview
|
|
|
|
- `agents.list`: named agent definitions (workspace, model, etc.).
|
|
- `bindings`: map inbound channels/accounts/peers to agents.
|
|
|
|
Example:
|
|
|
|
```json5
|
|
{
|
|
agents: {
|
|
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" },
|
|
],
|
|
}
|
|
```
|
|
|
|
## Session storage
|
|
|
|
Runtime session rows live in each agent's SQLite database under the state
|
|
directory (default `~/.openclaw`):
|
|
|
|
- `~/.openclaw/agents/<agentId>/agent/openclaw-agent.sqlite`
|
|
|
|
Older installs may have legacy transcript JSONL files and a `sessions.json` row
|
|
store under `~/.openclaw/agents/<agentId>/sessions/`. Gateway startup and
|
|
`openclaw doctor --fix` import hot legacy rows/history into SQLite
|
|
automatically. Use `openclaw doctor --session-sqlite inspect
|
|
--session-sqlite-all-agents` and the
|
|
[Doctor](/cli/doctor#session-sqlite-migration) validation sequence when you need
|
|
explicit migration evidence.
|
|
You can still select a legacy store path via `session.store` and `{agentId}`
|
|
templating for migration and offline-maintenance workflows.
|
|
|
|
Gateway and ACP session discovery also scans disk-backed agent stores under the
|
|
default `agents/` root and under templated `session.store` roots. Discovered
|
|
stores must stay inside that resolved agent root and use a regular legacy
|
|
`sessions.json` file. Symlinks and out-of-root paths are ignored.
|
|
|
|
## WebChat behavior
|
|
|
|
WebChat attaches to the **selected agent** and defaults to the agent's main
|
|
session. Because of this, WebChat lets you see cross-channel context for that
|
|
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.
|
|
|
|
This is consistent across channels.
|
|
|
|
## Related
|
|
|
|
- [Groups](/channels/groups)
|
|
- [Broadcast groups](/channels/broadcast-groups)
|
|
- [Pairing](/channels/pairing)
|