docs/reference/session-management-compaction.md
OpenClaw manages sessions end-to-end across these areas:
sessionKey)sessions.json) and what it tracks*.jsonl) and its structureIf you want a higher-level overview first, start with:
OpenClaw is designed around a single Gateway process that owns session state.
OpenClaw persists sessions in two layers:
Session store (sessions.json)
sessionKey -> SessionEntryTranscript (<sessionId>.jsonl)
id + parentId).checkpoint.*.jsonl copy.Gateway history readers should avoid materializing the whole transcript unless
the surface explicitly needs arbitrary historical access. First-page history,
embedded chat history, restart recovery, and token/usage checks use bounded tail
reads. Full transcript scans go through the async transcript index, which is
cached by file path plus mtimeMs/size and shared across concurrent readers.
Per agent, on the Gateway host:
~/.openclaw/agents/<agentId>/sessions/sessions.json~/.openclaw/agents/<agentId>/sessions/<sessionId>.jsonl
.../<sessionId>-topic-<threadId>.jsonlOpenClaw resolves these via src/config/sessions.ts.
Session persistence has automatic maintenance controls (session.maintenance) for sessions.json, transcript artifacts, and trajectory sidecars:
mode: warn (default) or enforcepruneAfter: stale-entry age cutoff (default 30d)maxEntries: cap entries in sessions.json (default 500)resetArchiveRetention: retention for *.reset.<timestamp> transcript archives (default: same as pruneAfter; false disables cleanup)maxDiskBytes: optional sessions-directory budgethighWaterBytes: optional target after cleanup (default 80% of maxDiskBytes)Normal Gateway writes flow through a per-store session writer that serializes in-process mutations without taking a runtime file lock. Hot-path patch helpers borrow the validated mutable cache while they hold that writer slot, so large sessions.json files are not cloned or reread for every metadata update. Runtime code should prefer updateSessionStore(...) or updateSessionStoreEntry(...); direct whole-store saves are compatibility and offline-maintenance tools. When a Gateway is reachable, non-dry-run openclaw sessions cleanup and openclaw agents delete delegate store mutations to the Gateway so cleanup joins the same writer queue; --store <path> is the explicit offline repair path for direct file maintenance. maxEntries cleanup is still batched for production-sized caps, so a store may briefly exceed the configured cap before the next high-water cleanup rewrites it back down. Session store reads do not prune or cap entries during Gateway startup; use writes or openclaw sessions cleanup --enforce for cleanup. openclaw sessions cleanup --enforce still applies the configured cap immediately.
Maintenance keeps durable external conversation pointers such as group sessions and thread-scoped chat sessions, but synthetic runtime entries for cron, hooks, heartbeat, ACP, and sub-agents can still be removed when they exceed the configured age, count, or disk budget.
OpenClaw no longer creates automatic sessions.json.bak.* rotation backups during Gateway writes. The legacy session.maintenance.rotateBytes key is ignored and openclaw doctor --fix removes it from older configs.
Transcript mutations use a session write lock on the transcript file. Lock acquisition waits up to
session.writeLock.acquireTimeoutMs before surfacing a busy-session error; the default is 60000
ms. Raise this only when legitimate prep, cleanup, compaction, or transcript mirror work contends
longer on slow machines. Stale-lock detection and maximum hold warnings remain separate policies.
Enforcement order for disk budget cleanup (mode: "enforce"):
highWaterBytes.In mode: "warn", OpenClaw reports potential evictions but does not mutate the store/files.
Run maintenance on demand:
openclaw sessions cleanup --dry-run
openclaw sessions cleanup --enforce
Isolated cron runs also create session entries/transcripts, and they have dedicated retention controls:
cron.sessionRetention (default 24h) prunes old isolated cron run sessions from the session store (false disables).cron.runLog.maxBytes + cron.runLog.keepLines prune ~/.openclaw/cron/runs/<jobId>.jsonl files (defaults: 2_000_000 bytes and 2000 lines).When cron force-creates a new isolated run session, it sanitizes the previous
cron:<jobId> session entry before writing the new row. It carries safe
preferences such as thinking/fast/verbose settings, labels, and explicit
user-selected model/auth overrides. It drops ambient conversation context such
as channel/group routing, send or queue policy, elevation, origin, and ACP
runtime binding so a fresh isolated run cannot inherit stale delivery or
runtime authority from an older run.
sessionKey)A sessionKey identifies which conversation bucket you’re in (routing + isolation).
Common patterns:
agent:<agentId>:<mainKey> (default main)agent:<agentId>:<channel>:group:<id>agent:<agentId>:<channel>:channel:<id> or ...:room:<id>cron:<job.id>hook:<uuid> (unless overridden)The canonical rules are documented at /concepts/session.
sessionId)Each sessionKey points at a current sessionId (the transcript file that continues the conversation).
Rules of thumb:
/new, /reset) creates a new sessionId for that sessionKey.sessionId on the next message after the reset boundary.session.reset.idleMinutes or legacy session.idleMinutes) creates a new sessionId when a message arrives after the idle window. When daily + idle are both configured, whichever expires first wins.session.parentForkMaxTokens config is removed by openclaw doctor --fix.Implementation detail: the decision happens in initSessionState() in src/auto-reply/reply/session.ts.
sessions.json)The store’s value type is SessionEntry in src/config/sessions.ts.
Key fields (not exhaustive):
sessionId: current transcript id (filename is derived from this unless sessionFile is set)sessionStartedAt: start timestamp for the current sessionId; daily reset
freshness uses this. Legacy rows may derive it from the JSONL session header.lastInteractionAt: last real user/channel interaction timestamp; idle reset
freshness uses this so heartbeat, cron, and exec events do not keep sessions
alive. Legacy rows without this field fall back to the recovered session start
time for idle freshness.updatedAt: last store-row mutation timestamp, used for listing, pruning, and
bookkeeping. It is not the authority for daily/idle reset freshness.sessionFile: optional explicit transcript path overridechatType: direct | group | room (helps UIs and send policy)provider, subject, room, space, displayName: metadata for group/channel labelingthinkingLevel, verboseLevel, reasoningLevel, elevatedLevelsendPolicy (per-session override)providerOverride, modelOverride, authProfileOverrideinputTokens, outputTokens, totalTokens, contextTokenscompactionCount: how often auto-compaction completed for this session keymemoryFlushAt: timestamp for the last pre-compaction memory flushmemoryFlushCompactionCount: compaction count when the last flush ranThe store is safe to edit, but the Gateway is the authority: it may rewrite or rehydrate entries as sessions run.
*.jsonl)Transcripts are managed by @mariozechner/pi-coding-agent’s SessionManager.
The file is JSONL:
type: "session", includes id, cwd, timestamp, optional parentSession)id + parentId (tree)Notable entry types:
message: user/assistant/toolResult messagescustom_message: extension-injected messages that do enter model context (can be hidden from UI)custom: extension state that does not enter model contextcompaction: persisted compaction summary with firstKeptEntryId and tokensBeforebranch_summary: persisted summary when navigating a tree branchOpenClaw intentionally does not “fix up” transcripts; the Gateway uses SessionManager to read/write them.
Two different concepts matter:
sessions.json (used for /status and dashboards)If you’re tuning limits:
contextTokens in the store is a runtime estimate/reporting value; don’t treat it as a strict guarantee.For more, see /token-use.
Compaction summarizes older conversation into a persisted compaction entry in the transcript and keeps recent messages intact.
After compaction, future turns see:
firstKeptEntryIdCompaction is persistent (unlike session pruning). See /concepts/session-pruning.
When OpenClaw splits a long transcript into compaction chunks, it keeps
assistant tool calls paired with their matching toolResult entries.
In the embedded Pi agent, auto-compaction triggers in two cases:
request_too_large, context length exceeded, input exceeds the maximum number of tokens, input token count exceeds the maximum number of input tokens, input is too long for the model, ollama error: context length exceeded, and similar provider-shaped variants) → compact → retry.contextTokens > contextWindow - reserveTokens
Where:
contextWindow is the model’s context windowreserveTokens is headroom reserved for prompts + the next model outputThese are Pi runtime semantics (OpenClaw consumes the events, but Pi decides when to compact).
OpenClaw can also trigger a preflight local compaction before opening the next
run when agents.defaults.compaction.maxActiveTranscriptBytes is set and the
active transcript file reaches that size. This is a file-size guard for local
reopen cost, not raw archival: OpenClaw still runs normal semantic compaction,
and it requires truncateAfterCompaction so the compacted summary can become a
new successor transcript.
For embedded Pi runs, agents.defaults.compaction.midTurnPrecheck.enabled: true
adds an opt-in tool-loop guard. After a tool result is appended and before the
next model call, OpenClaw estimates the prompt pressure using the same preflight
budget logic used at turn start. If the context no longer fits, the guard does
not compact inside Pi's transformContext hook. It raises a structured
mid-turn precheck signal, stops the current prompt submission, and lets the
outer run loop use the existing recovery path: truncate oversized tool results
when that is enough, or trigger the configured compaction mode and retry. The
option is disabled by default and works with both default and safeguard
compaction modes, including provider-backed safeguard compaction.
This is independent of maxActiveTranscriptBytes: the byte-size guard runs
before a turn opens, while mid-turn precheck runs later in the embedded Pi tool
loop after new tool results have been appended.
reserveTokens, keepRecentTokens)Pi’s compaction settings live in Pi settings:
{
compaction: {
enabled: true,
reserveTokens: 16384,
keepRecentTokens: 20000,
},
}
OpenClaw also enforces a safety floor for embedded runs:
compaction.reserveTokens < reserveTokensFloor, OpenClaw bumps it.20000 tokens.agents.defaults.compaction.reserveTokensFloor: 0 to disable the floor./compact honors an explicit agents.defaults.compaction.keepRecentTokens
and keeps Pi's recent-tail cut point. Without an explicit keep budget,
manual compaction remains a hard checkpoint and rebuilt context starts from
the new summary.agents.defaults.compaction.midTurnPrecheck.enabled: true to run the
optional tool-loop precheck after new tool results and before the next model
call. This is a trigger only; summary generation still uses the configured
compaction path. It is independent of maxActiveTranscriptBytes, which is a
turn-start active-transcript byte-size guard.agents.defaults.compaction.maxActiveTranscriptBytes to a byte value or
string such as "20mb" to run local compaction before a turn when the active
transcript gets large. This guard is active only when
truncateAfterCompaction is also enabled. Leave it unset or set 0 to
disable.agents.defaults.compaction.truncateAfterCompaction is enabled,
OpenClaw rotates the active transcript to a compacted successor JSONL after
compaction. The old full transcript remains archived and linked from the
compaction checkpoint instead of being rewritten in place.Why: leave enough headroom for multi-turn “housekeeping” (like memory writes) before compaction becomes unavoidable.
Implementation: ensurePiCompactionReserveTokens() in src/agents/pi-settings.ts
(called from src/agents/pi-embedded-runner.ts).
Plugins can register a compaction provider via registerCompactionProvider() on the plugin API. When agents.defaults.compaction.provider is set to a registered provider id, the safeguard extension delegates summarization to that provider instead of the built-in summarizeInStages pipeline.
provider: id of a registered compaction provider plugin. Leave unset for default LLM summarization.provider forces mode: "safeguard".qualityGuard.enabled: false to skip retry-on-malformed-output behavior.Source: src/plugins/compaction-provider.ts, src/agents/pi-hooks/compaction-safeguard.ts.
You can observe compaction and session state via:
/status (in any chat session)openclaw status (CLI)openclaw sessions / sessions --json🧹 Auto-compaction complete + compaction countNO_REPLY)OpenClaw supports “silent” turns for background tasks where the user should not see intermediate output.
Convention:
NO_REPLY /
no_reply to indicate “do not deliver a reply to the user”.NO_REPLY and
no_reply both count when the whole payload is just the silent token.As of 2026.1.10, OpenClaw also suppresses draft/typing streaming when a
partial chunk begins with NO_REPLY, so silent operations don’t leak partial
output mid-turn.
Goal: before auto-compaction happens, run a silent agentic turn that writes durable
state to disk (e.g. memory/YYYY-MM-DD.md in the agent workspace) so compaction can’t
erase critical context.
OpenClaw uses the pre-threshold flush approach:
NO_REPLY / no_reply so the user sees
nothing.Config (agents.defaults.compaction.memoryFlush):
enabled (default: true)model (optional exact provider/model override for the flush turn, for example ollama/qwen3:8b)softThresholdTokens (default: 4000)prompt (user message for the flush turn)systemPrompt (extra system prompt appended for the flush turn)Notes:
NO_REPLY hint to suppress
delivery.model is set, the flush turn uses that model without inheriting the
active session fallback chain, so local-only housekeeping does not silently
fall back to a paid conversation model.sessions.json).workspaceAccess: "ro" or "none").Pi also exposes a session_before_compact hook in the extension API, but OpenClaw’s
flush logic lives on the Gateway side today.
sessionKey in /status.openclaw status.reserveTokens too high for the model window can cause earlier compaction)NO_REPLY (case-insensitive exact token) and you’re on a build that includes the streaming suppression fix.