Skip to main content

Session Management

WednesdayAI 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.
Security Warning: If your agent can receive DMs from multiple people, you should strongly consider enabling secure DM mode. Without it, all users share the same conversation context, which can leak private information between users.
Example of the problem with default settings:
  • Alice (<SENDER_A>) messages your agent about a private topic (for example, a medical appointment)
  • Bob (<SENDER_B>) messages your agent asking “What were we talking about?”
  • Because both DMs share the same session, the model may answer Bob using Alice’s prior context.
The fix: Set dmScope to isolate sessions per user:
When to enable this:
  • You have pairing approvals for more than one sender
  • You use a DM allowlist with multiple entries
  • You set dmPolicy: "open"
  • Multiple phone numbers or accounts can message your agent
Notes:
  • Default is dmScope: "main" for continuity (all DMs share the main session). This is fine for single-user setups.
  • Local CLI onboarding writes session.dmScope: "per-channel-peer" by default when unset (existing explicit values are preserved).
  • For multi-account inboxes on the same channel, prefer per-account-channel-peer.
  • If the same person contacts you on multiple channels, use session.identityLinks to collapse their DM sessions into one canonical identity.
  • You can verify your DM settings with openclaw security audit (see security).
  • Session isolation and Workspace Lanes solve different problems. dmScope separates conversation history; workspace lanes separate user-specific workspace files for multi-user-capable personas.

Gateway is the source of truth

All session state is owned by the gateway. 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 gateway’s 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).
  • The store is a map sessionKey -> { sessionId, updatedAt, ... }. Deleting entries is safe; they are recreated on demand.
  • Group entries may include displayName, channel, subject, room, and space to label sessions in UIs.
  • Session entries include origin metadata (label + routing hints) so UIs can explain where a session came from.
  • WednesdayAI does not read legacy Pi/Tau session folders.

Maintenance

WednesdayAI applies session-store maintenance to keep sessions.json and transcript artifacts bounded over time.

Defaults

  • session.maintenance.mode: warn
  • session.maintenance.pruneAfter: 30d
  • session.maintenance.maxEntries: 500
  • session.maintenance.rotateBytes: 10mb
  • session.maintenance.resetArchiveRetention: defaults to pruneAfter (30d)
  • session.maintenance.maxDiskBytes: unset (disabled)
  • session.maintenance.highWaterBytes: defaults to 80% of maxDiskBytes when budgeting is enabled

How it works

Maintenance runs during session-store writes, and you can trigger it on demand with openclaw sessions cleanup.
  • mode: "warn": reports what would be evicted but does not mutate entries/transcripts.
  • mode: "enforce": applies cleanup in this order:
    1. prune stale entries older than pruneAfter
    2. cap entry count to maxEntries (oldest first)
    3. archive transcript files for removed entries that are no longer referenced
    4. purge old *.deleted.<timestamp> and *.reset.<timestamp> archives by retention policy
    5. rotate sessions.json when it exceeds rotateBytes
    6. if maxDiskBytes is set, enforce disk budget toward highWaterBytes (oldest artifacts first, then oldest sessions)

Performance caveat for large stores

Large session stores are common in high-volume setups. Maintenance work is write-path work, so very large stores can increase write latency. What increases cost most:
  • very high session.maintenance.maxEntries values
  • long pruneAfter windows that keep stale entries around
  • many transcript/archive artifacts in ~/.openclaw/agents/<agentId>/sessions/
  • enabling disk budgets (maxDiskBytes) without reasonable pruning/cap limits
What to do:
  • use mode: "enforce" in production so growth is bounded automatically
  • set both time and count limits (pruneAfter + maxEntries), not just one
  • set maxDiskBytes + highWaterBytes for hard upper bounds in large deployments
  • keep highWaterBytes meaningfully below maxDiskBytes (default is 80%)
  • run openclaw sessions cleanup --dry-run --json after config changes to verify projected impact before enforcing
  • for frequent active sessions, pass --active-key when running manual cleanup

Customize examples

Use a conservative enforce policy:
Enable a hard disk budget for the sessions directory:
Tune for larger installs (example):
Preview or force maintenance from CLI:

Plugin correlation and privacy

Plugins can receive resolved runtime identity and turn correlation fields so they can log, dedupe, and run background analysis without parsing session keys or reading raw identity configuration. For operators, the important boundary is:
  • session.identityLinks remains configuration used by core identity resolution. Plugin contexts expose resolved fields such as canonicalIdentity and senderIsOwner; they do not expose raw identityLinks.
  • channelConversationId identifies the transport conversation or thread. Legacy plugin fields named conversationId refer to this channel identity.
  • providerConversationId is an opaque provider/cache correlation id derived by WednesdayAI for model calls. It is not a session storage column and should be stored only as an opaque value.
  • turn.turnId identifies one user turn and is the same string value as runId in the v1 plugin contract.
  • sessionId and sessionKey may be absent during very early message_received hooks. WednesdayAI omits them when no real session exists instead of fabricating a session value.
If a plugin captures storage append events, user and assistant entries for the same exchange share one turnId. Dedupe append fan-out with the turn id plus the entry id, cursor entry id, or raw entry hash. This feature does not make untrusted plugins safe. Plugins still run in-process with gateway trust, but the public SDK now gives them resolved identity and opaque correlation fields instead of requiring them to read private config or reverse-engineer session key formats.

External session storage

By default, sessions live in ~/.openclaw/agents/<agentId>/sessions/ on the gateway host as local JSONL transcript files plus the sessions.json store index. For durable or shared storage, configure session.storage to use SQLite or Postgres, optionally with a Redis hot cache. See the session.storage section in Gateway configuration and the full Configuration Reference.

Configured-backend-only invariant

WednesdayAI treats the configured session.storage.backend as the single source of truth for session transcript data:
  • fs-jsonl (legacy default): transcript entries are read and written through *.jsonl files.
  • sqlite: transcript entries are read and written through the SQLite ctx_session_entries table.
  • postgres: transcript entries are read and written through the Postgres ctx_session_entries table.
The JSONL path is reachable only when backend === "fs-jsonl". When a database backend is active, core paths such as chat.history, sessions.preview, session-list title/preview derivation, and sub-agent announce output read from the configured database, not from JSONL files. What this means in practice:
  • Switching from JSONL to SQLite/Postgres does not require a manual transcript migration; the store metadata file is rebuilt on demand and the database becomes the source of truth for content.
  • If you have existing .jsonl files and start with a database backend, the gateway may backfill those files into the database on first startup, but subsequent reads always come from the database. Disable the backfill with session.storage.migration.validateOnStart: false if you want a clean cut-over.
  • Running openclaw sessions cleanup still bounds sessions.json and any JSONL archives for the fs-jsonl backend; for database backends it does not mutate the database content directly.

Sub-agent and announce output

Sub-agent runs (sessions_spawn) and their announce step use the same transcript read path as the rest of the gateway. Before this invariant, a database backend could leave sub-agent assistant output in Postgres while the announce reader looked at JSONL, producing a truncated or empty “(no output)” result. With the configured-backend-only invariant, the announce reader consults the same backend that the run wrote to, so sub-agent output renders correctly regardless of whether the backend is JSONL, SQLite, or Postgres.

Session pruning

WednesdayAI 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.

Pre-compaction memory flush

When a session nears auto-compaction, WednesdayAI 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 and 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.
    • per-peer: agent:<agentId>:dm:<peerId>.
    • per-channel-peer: agent:<agentId>:<channel>:dm:<peerId>.
    • per-account-channel-peer: agent:<agentId>:<channel>:<accountId>:dm:<peerId> (accountId defaults to default).
    • If session.identityLinks matches a provider-prefixed peer id (for example telegram:123), the canonical key replaces <peerId> so the same person shares a session across channels.
  • Group chats isolate state: agent:<agentId>:<channel>:group:<id> (rooms/channels use agent:<agentId>:<channel>:channel:<id>).
    • Telegram forum topics append :topic:<threadId> to the group id for isolation.
    • Legacy group:<id> keys are still recognized for migration.
  • Inbound contexts may still use group:<id>; the channel is inferred from Provider and normalized to the canonical agent:<agentId>:<channel>:group:<id> form.
  • Other sources:
    • Isolated cron assistant-task jobs: cron:<job.id>
    • Webhooks: hook:<uuid> (unless explicitly set by the hook)
    • Node runs: node-<nodeId>

Lifecycle

  • Reset policy: sessions are reused until they expire, and expiry is evaluated on the next inbound message.
  • Daily reset: defaults to 4:00 AM local time on the gateway host. A session is stale once its last update is earlier than the most recent daily reset time.
  • Idle reset (optional): idleMinutes adds a sliding idle window. When both daily and idle resets are configured, whichever expires first forces a new session.
  • Legacy idle-only: if you set session.idleMinutes without any session.reset/resetByType config, WednesdayAI stays in idle-only mode for backward compatibility.
  • Per-type overrides (optional): resetByType lets you override the policy for direct, group, and thread sessions (thread = Slack/Discord threads, Telegram topics, Matrix threads when provided by the connector).
  • Per-channel overrides (optional): resetByChannel overrides the reset policy for a channel (applies to all session types for that channel and takes precedence over reset/resetByType).
  • Reset triggers: exact /new or /reset (plus any extras in resetTriggers) start a fresh session id and pass the remainder of the message through. /new <model> accepts a model alias, provider/model, or provider name (fuzzy match) to set the new session model. If /new or /reset is sent alone, WednesdayAI runs a short “hello” greeting turn to confirm the reset.
  • Manual reset: delete specific keys from the store or remove the JSONL transcript; the next message recreates them.
  • Isolated cron assistant-task jobs always mint a fresh sessionId per run (no idle reuse). Main-session cron jobs add a system event or wake-gate nudge to the main heartbeat path instead of creating a cron:<job.id> chat session.

Send policy (optional)

Block delivery for specific session types without listing individual ids.
Runtime override (owner only):
  • /send on → allow for this session
  • /send off → deny for this session
  • /send inherit → clear override and use config rules Send these as standalone messages so they register.

Configuration (optional rename example)

Inspecting

  • openclaw status — shows store path and recent sessions.
  • openclaw sessions --json — dumps every entry (filter with --active <minutes>).
  • openclaw gateway call sessions.list --params '{}' — fetch sessions from the running gateway (use --url/--token for remote gateway access).
  • Send /status as a standalone message in chat to see whether the agent is reachable, how much of the session context is used, current thinking/verbose toggles, and when your WhatsApp web creds were last refreshed (helps spot relink needs).
  • Send /context list or /context detail to see what’s in the system prompt and injected workspace files (and the biggest context contributors).
  • Send /stop (or standalone abort phrases like stop, stop action, stop run, stop openclaw) to abort the current run, clear queued followups for that session, and stop any sub-agent runs spawned from it (the reply includes the stopped count).
  • Send /compact (optional instructions) as a standalone message to summarize older context and free up window space. See /concepts/compaction.
  • 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.

Troubleshooting

Two different people share context in DMs (privacy leak) dmScope: "main" is the default and routes all direct messages into one session. Set dmScope: "per-channel-peer" (or "per-account-channel-peer" for multi-account inboxes) so each sender gets an isolated context. Run openclaw security audit to confirm your DM isolation settings. Session does not reset at the expected daily time The daily reset uses the gateway host’s local time (default 4:00 AM). If the gateway host is in a different timezone than expected, the reset fires at the wrong wall-clock time. Adjust session.reset.atHour to match your preferred local time, or set the gateway host’s timezone. Also verify the gateway restarted after the config change. sessions.json grows very large and write latency increases Enable mode: "enforce" and set both pruneAfter and maxEntries limits. Running maintenance only in "warn" mode lets the store grow unbounded. Use openclaw sessions cleanup --dry-run --json to preview the impact before enforcing. For hard upper bounds, add maxDiskBytes + highWaterBytes.

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).