Session Handling
July 2, 2026 · View on GitHub
This doc describes what an OpenClaw session is, how its lifecycle is managed, and what each session-related tool (sessions_spawn, sessions_yield, sessions) actually does. It is aimed at contributors and operators who see these tools during request processing and want to understand the model underneath.
What a session is
A session is the unit of conversational state that the gateway routes messages to. It is defined in src/OpenClaw.Core/Models/Session.cs:
- Identity:
Id,ChannelId,SenderId(lines 22-24). - Conversation:
History— an orderedList<ChatTurn>of{ Role, Content, Timestamp, ToolCalls? }(line 27, turn shape at lines 137-143). - Lifecycle state:
SessionStateenum —Active,Paused,Expired(line 28, enum at 130-135). - Timestamps:
CreatedAt,LastActiveAt(lines 25-26) —LastActiveAtis what drives expiry. - Per-session overrides: model, reasoning effort, tool preset, system prompt, route-scoped allowlist, contract policy, delegation metadata. These let one session opt into different behavior than the gateway default without polluting other sessions.
- Token counters:
TotalInputTokens,TotalOutputTokens, cache-read/write tokens — updated atomically viaInterlocked(lines 60-86) so cost accounting is thread-safe.
The default key for a session is channelId:senderId, so "a given user on a given channel" maps to one session by default. Explicit session IDs are used for sub-agent sessions, cron jobs, webhooks, and anything else that needs a stable named session independent of a user.
The owner: SessionManager
src/OpenClaw.Core/Sessions/SessionManager.cs is the single owner of session state. It is an IAsyncDisposable singleton with:
ConcurrentDictionary<string, Session> _active— the in-memory cache of live sessions (line 15).IMemoryStore _store— the persistent backing store (SQLite in the default setup)._timeout(fromGatewayConfig.SessionTimeoutMinutes) — idle timeout before a session is swept._maxSessions(fromGatewayConfig.MaxConcurrentSessions) — hard cap on the in-memory active set._admissionGate— a single-permit semaphore serializing admission so capacity accounting is race-free.
The manager's public surface that matters for the lifecycle:
| Method | Purpose | File ref |
|---|---|---|
GetOrCreateAsync(channelId, senderId, ct) | Default admission path. Key = channelId:senderId. | line 45 |
GetOrCreateByIdAsync(sessionId, channelId, senderId, ct) | Admission with an explicit ID. Used by sessions_spawn, cron, webhooks. | line 55 |
TryGetActiveById(sessionId) | Non-allocating active-cache lookup. Used by sessions_yield polling. | line 273 |
LoadAsync(sessionId, ct) | Active cache with store fallback. | line 297 |
ListActiveAsync(ct) | Snapshot of the active set (used by sessions list). | line 255 |
PersistAsync(session, ct) | Save to store with retry/backoff (3 attempts, exponential). | line 132 |
RemoveActive(sessionId) | Evict from the active cache; session remains in the store. | line 309 |
SweepExpiredActiveSessions() | Bulk eviction of sessions past _timeout. | line 338 |
EnsureCapacityForAdmission() | Called under _admissionGate; sweeps expired sessions, then evicts the stalest if still at _maxSessions. | line 443 |
Lifecycle, step by step
-
Admission. A new message, or
sessions_spawn, callsGetOrCreateByIdAsync. Fast path: if the session is already in_active, bumpLastActiveAtand return it (lines 63-67). Slow path: acquire_admissionGate, re-check the cache, then try to rehydrate from the store (_store.GetSessionAsyncat line 81). If nothing exists yet, a newSession { State = Active }is constructed at lines 104-110. Before insertion,EnsureCapacityForAdmission()runs — it sweeps expired sessions first and only evicts non-expired sessions (oldest-LastActiveAtwins) if the cap is still breached. -
Active work. Every request that resolves a session touches
LastActiveAt. Turns are appended toHistoryas the agent produces them. Token counters are updated atomically viaAddTokenUsage/AddCacheUsage. The manager takes a per-sessionSemaphoreSlim(_sessionLocks) when mutating state that must be serialized against persistence. -
Persistence.
PersistAsyncwrites the session throughIMemoryStore.SaveSessionAsyncunder the per-session lock, with up to 3 attempts and exponential backoff on transient failures (lines 144-165). Writes happen after turn completion and opportunistically in the background — there is no "commit the session" call from tools. -
Checkpointing during long turns. During multi-step agent execution, the native runtime writes
Session.ExecutionCheckpointimmediately after each completed tool batch. The checkpoint records the kind (tool_batch), resume state, sequence, iteration, history count, correlation id, timestamps, and per-tool metadata. The corresponding tool arguments/results remain in the persistedHistorytool-use turn, so a restarted runtime can rebuild the last completed batch without calling those tools again. When a later message arrives for a session whose latest checkpoint isready_to_resumeand whose history still ends at that checkpoint, the runtime resumes from that checkpoint instead of appending a new user turn. A bareresume,continue,/resume, or/continuemessage is treated as the resume trigger; any other text is passed as a resume note. -
Inter-session routing. Nothing about a session is tied to a single thread or request. All session traffic flows through
MessagePipeline.InboundWriterkeyed bySessionId. That means every tool that wants to "talk to another session" is really just queuing anInboundMessagewith the target session's ID — the runtime picks it up and runs that session's turn the same way a human message would. -
Expiry and eviction. Two forces remove sessions from the active cache:
- Time.
SweepExpiredActiveSessionsmarks anything idle past_timeoutasExpired, removes it from_active, and decrements_activeCount. Sweeping is triggered both on a background cadence and insideEnsureCapacityForAdmissionbefore any forced eviction. - Capacity. If admission would push
_activeCountover_maxSessionsafter sweeping, the session with the oldestLastActiveAtis evicted (RemoveActive).
Eviction is not deletion — the session remains in the store. The next message for that ID simply takes the rehydrate path in step 1.
- Time.
-
Disposal. On shutdown, the manager awaits any in-flight background persistence tasks and disposes per-session semaphores.
The tools
sessions_spawn — fire-and-forget
Gateway tool defined in src/OpenClaw.Gateway/Tools/SessionsSpawnTool.cs.
- Args:
prompt(required), optionalsession_idandchannel_id. - Effect: creates or retrieves a session with the given ID via
GetOrCreateByIdAsync(line 43), then writes the initial prompt to the inbound pipeline as a system message (lines 45-54). - Returns: immediately — the new session ID. The spawned agent processes asynchronously.
Use when the parent does not need the child's reply to continue its own turn.
sessions_yield — synchronous rendezvous
Gateway tool defined in src/OpenClaw.Gateway/Tools/SessionsYieldTool.cs.
- Args:
session_id(required),message(required),timeout_seconds(default 60, clamped 5–300). - Effect: refuses self-yield (deadlock guard at lines 43-44), snapshots
target.History.Count, queues the message through the pipeline (line 72), then polls the target session for a newassistantturn past the snapshot. Poll delay starts at 500 ms and backs off to 2 s (lines 80-85). If the target is evicted mid-wait, the tool falls back to the store once (lines 91-95). - Returns: the target's assistant reply, or a timeout message.
This is effectively a synchronous RPC between two sessions on top of the same async message pipeline.
sessions — list / history / send
Agent tool defined in src/OpenClaw.Agent/Tools/SessionsTool.cs.
list: returns every active session withId,ChannelId,SenderId,State. Backed byListActiveAsync.history: returns the last N turns (limit, default 10 for the agent tool) of a named session, resolved viaLoadAsyncso evicted sessions still read correctly.send: queues anInboundMessagefor the named session and returns immediately — identical plumbing tosessions_spawn, just targeting an existing session.
There is no yield-equivalent on this agent-facing tool; use the gateway-facing sessions_yield for synchronous waits.
Registration
All three are grouped under the group:sessions tool preset in src/OpenClaw.Gateway/ToolPresetResolver.cs (line 52). Operators can enable or restrict the whole cluster with one preset entry.
Mental model
- Sessions are state, not threads. They are just rows keyed by ID with a conversation list and some overrides. Nothing in
SessionManagerowns an execution context. - Checkpoints are save points, not full runtime snapshots. They are written after completed tool batches, which is the first durable point where OpenClaw can resume without duplicating already-run tools. They intentionally do not snapshot every internal token, stream, or provider transition.
- The pipeline is the only way in.
sessions_spawn,sessions_yield,sessions_send,background_auto_continue, andbackground_auto_resumeall produce the sameInboundMessageshape that a user's channel message produces. Sub-agent sessions and background continuations are not separate runtime paths. - Sessions can continue in the background. When the current turn reaches the max iteration limit (
MaxIterationsPerBatch, default 20),RunTurnAsyncreturnsBatchLimitReachedwithShouldContinue=trueregardless of Goal state. The Gateway initializesBackgroundRunon first continuation and writes abackground_auto_continueinternal message back into the pipeline. The session continues executing in bounded batches with separate concurrency limits (MaxConcurrentBackgroundTurns, default 3). If an active Goal is present, the continuation prompt includes goal-specific instructions. WebSocket disconnects and Channel client exits do not cancel background work. Background execution is gated byBackgroundExecution.Enabled(default true). - Startup recovery re-enqueues runnable sessions. Gateway startup scans the persistent store for sessions with
RunState=Running|Continuingand an active Goal, then re-enqueues them with staggered concurrency. - Eviction is a cache decision. Hitting the active cap or going idle past the timeout removes a session from memory, not from durable storage. The next message rehydrates it.
- Spawn vs yield is the async/sync axis: spawn returns immediately; yield polls for a reply with a bounded timeout.
- Use
sessions list/sessions historywhen you want to introspect state without sending a message.
Per-turn Token Accounting
OpenClaw tracks token usage at multiple levels during each turn. The same model output can update turn-local diagnostics, session totals, runtime counters, provider aggregates, and (when enabled) contract-governance cost tracking.
How one turn is accounted
- Turn context is established.
AgentRuntimecreates aTurnContextfor correlation and per-turn observability before model/tool execution begins. - Usage is ingested at turn accounting boundaries.
AgentTurnAccountingrecords usage for non-streaming and streaming paths, normalizing input/output/cache components when available. - Fallback estimation is applied when needed. If a provider does not return usage on a streaming response, runtime estimation can backfill token counts for accounting continuity.
- The same turn is written to multiple sinks.
Sessioncounters (TotalInputTokens,TotalOutputTokens, cache counters)RuntimeMetricsglobal process countersProviderUsageTrackerprovider/model totals and recent-turn entries- Contract governance turn-cost tracking when contract mode is active
- Operator/user surfaces read from those sinks.
/status,/usage, metrics/admin endpoints, and OpenAI-compatible usage fields are all projections over these counters.
Where to observe token usage
- Turn level:
TurnContextsummaries and per-turn logging. - Session cumulative:
/status,/usage, and session-bound summaries. - Runtime/provider counters:
/metrics,/metrics/providers, and provider snapshots. - Operator investigation views:
/admin/providersand/admin/sessions/{id}/timeline. - Compatibility responses: OpenAI-compatible
usagepayloads returned by chat/responses endpoints.
Per-session Task Token Ledger (Persistent)
OpenClaw now records each turn's token usage as an append-only audit stream, so "every session task's token consumption" can be traced beyond the in-memory recent-turn window.
- Write model: append-only JSONL, one line per turn.
- Default file path:
<Memory.StoragePath>/audit/turn-token-usage.jsonl. - Record shape:
TurnTokenUsageRecord(CorrelationId,SessionId,ChannelId,ProviderId,ModelId, input/output/cache tokens,EstimatedInputTokensByComponent,IsEstimated,TimestampUtc). - Execution path: turn accounting emits
ITurnTokenUsageObserverrecords; default gateway wiring uses a composite observer that writes to bothProviderUsageTracker(bounded recent-turn investigative view) andTurnTokenUsageAuditLog(persistent append-only ledger).
Operational notes:
- This ledger is the durable source for per-turn/session-task audits.
- Dashboard provider timeline remains a bounded recent-turn view for troubleshooting.
IsEstimated=trueindicates provider usage was missing and accounting relied on estimation.
End-to-End Correlation ID Tracing
Each turn is assigned a CorrelationId that flows through the entire request pipeline, enabling three-way correlation:
- Structured logs — all log entries for a turn are tagged
[{CorrelationId}]. - Upstream provider headers — when
SendRequestMetadatais enabled, the correlation ID is forwarded as an HTTP header (defaultX-OpenClaw-Correlation-Id, configurable per model profile viaCorrelationIdHeader). - Persistent JSONL audit — each
TurnTokenUsageRecordinturn-token-usage.jsonlincludes theCorrelationIdfield.
External trace ID injection: Callers of the OpenAI-compatible /v1/chat/completions endpoint can pass an X-Request-Id or X-Trace-Id HTTP header. The gateway propagates this value as the turn's CorrelationId, enabling end-to-end distributed tracing from external systems through OpenClaw.NET to upstream LLM providers.
Viewing token usage in Dashboard
The visual entry point is the Sessions page in Dashboard (implemented in src/OpenClaw.Dashboard/Pages/Sessions.razor):
- Open Sessions. The left session list shows a
Σtotal token badge per session (input + output). - Select a session. The right detail panel shows token summary cards:
Input tokensOutput tokensCache read tokensCache write tokensTotal tokens
- In the same detail view, check Provider token timeline (backed by
/admin/sessions/{id}/timeline) for per-turn rows:- timestamp
- provider/model
- input/output/cache/total tokens
Semantics notes:
- Summary cards are cumulative session counters (
Session.Total*Tokens). - Timeline rows come from a bounded recent-turn provider window, intended for investigation rather than long-term audit.
- If upstream usage is missing on some paths, some token values may be estimated.
Current Semantics (Important)
- OpenAI-compatible
usagefields are currently emitted from session cumulative counters, not per-request deltas. - When upstream/provider usage is missing in some paths, token values may be estimated rather than provider-billed exact values.
- Provider recent-turn usage is a bounded in-memory window, not a long-term audit ledger.
- Per-turn persistent token audit is available via JSONL ledger (
turn-token-usage.jsonl). /statusand/usagereflect cumulative session counters, not just the most recent turn.
Implementation anchors
- Turn accounting entry points: src/OpenClaw.Agent/Runtime/AgentTurnAccounting.cs
- Session token/cache counters: src/OpenClaw.Core/Models/Session.cs
- Turn context observability: src/OpenClaw.Core/Observability/TurnContext.cs
- Provider aggregates and recent turns: src/OpenClaw.Core/Observability/ProviderUsageTracker.cs
- Turn token observer contract: src/OpenClaw.Core/Abstractions/ITurnTokenUsageObserver.cs
- Turn token record model: src/OpenClaw.Core/Models/TurnTokenUsageRecord.cs
- Persistent turn token ledger: src/OpenClaw.Core/Observability/TurnTokenUsageAuditLog.cs
- Runtime totals: src/OpenClaw.Core/Observability/RuntimeMetrics.cs
- Session command projections: src/OpenClaw.Core/Pipeline/ChatCommandProcessor.cs
- Metrics/admin endpoints: src/OpenClaw.Gateway/Endpoints/DiagnosticsEndpoints.cs, src/OpenClaw.Gateway/Endpoints/AdminEndpoints.Runtime.cs, src/OpenClaw.Gateway/Endpoints/AdminEndpoints.Sessions.cs
- Gateway observer wiring: src/OpenClaw.Gateway/Composition/CoreServicesExtensions.cs, src/OpenClaw.Gateway/Composition/RuntimeInitializationExtensions.RuntimeFactories.cs
- OpenAI-compatible usage serialization: src/OpenClaw.Gateway/Endpoints/OpenAiEndpoints.ChatCompletions.cs, src/OpenClaw.Gateway/Endpoints/OpenAiEndpoints.Responses.cs
Related
- TOOLS_GUIDE.md — the broader native tool catalog and how presets compose.
- USER_GUIDE.md — operator-facing view of channels, providers, and sessions.
- GLOSSARY.md — definitions of gateway, runtime, channel, profile, etc.
- PROMPT_CACHING.md — cache-read/cache-write usage semantics and provider-aware cache behavior.