OpenClaw Context Management Research¶
Last Updated: 2026-03-19
Source: openclaw/openclaw
Research focus: How OpenClaw assembles and manages context within a conversation, and how it differs from Pi (its underlying engine).
Architecture Overview¶
OpenClaw is built on top of Pi (pi-agent-core, pi-ai), but adds a substantial context management layer on top. Key source locations:
| Layer | Path | Role |
|---|---|---|
| Context Engine | src/context-engine/ |
Pluggable context assembly interface (7 lifecycle methods) |
| Agent Runtime | src/agents/pi-embedded-runner/run/attempt.ts |
Multi-stage context pipeline before each LLM call |
| System Prompt | src/agents/system-prompt.ts |
Complex multi-section prompt construction |
| Compaction | src/agents/pi-embedded-runner/compact.ts |
Delegates to Pi's compaction or custom engine |
| Subagent | src/agents/subagent-spawn.ts, subagent-registry.ts |
Built-in subagent lifecycle via gateway RPC |
| History | src/agents/pi-embedded-runner/history.ts |
Turn-based history truncation |
| Session Sanitize | src/agents/pi-embedded-runner/sanitize-session-history.ts |
Provider-specific message cleanup |
Context Assembly Flow¶
Unlike Pi's simple "accumulate all → send all → compact when full", OpenClaw has a multi-stage pipeline executed before each LLM call (attempt.ts:2130-2189):
messages (raw conversation history, accumulated like Pi)
│
▼ sanitizeSessionHistory() ← Clean: remove invalid tool results, fix pairing
│
▼ validateGeminiTurns() ← Provider-specific turn validation (Gemini rules)
▼ validateAnthropicTurns() ← Provider-specific turn validation (Anthropic rules)
│
▼ limitHistoryTurns() ← Config-based truncation (per DM/channel limit)
│
▼ sanitizeToolUseResultPairing() ← Repair orphaned tool results after truncation
│
▼ contextEngine.assemble() ← Pluggable: assemble context under token budget
│ ├── LegacyContextEngine: pass-through (no-op)
│ └── Custom engine: RAG, vector store, selective retrieval, etc.
│
▼ systemPromptAddition ← Context engine can inject extra system prompt text
│
▼ Send to LLM
Three Lines of Defense (vs Pi's One)¶
| Defense | Mechanism | Weight | Pi equivalent |
|---|---|---|---|
| 1st | limitHistoryTurns() |
Lightweight, hard truncation by turn count | None |
| 2nd | contextEngine.assemble() |
Pluggable, token-budget-aware assembly | transformContext hook (exists but unused) |
| 3rd | Compaction | Heavy, LLM-generated summary | Same (inherited from Pi) |
Pi only has the 3rd line. OpenClaw adds two layers before compaction can even trigger.
ContextEngine Interface¶
The pluggable contract (src/context-engine/types.ts) defines 7 lifecycle methods:
| Method | Required | Purpose |
|---|---|---|
bootstrap() |
Optional | Initialize engine state for a session, import historical context |
ingest() |
Required | Ingest a single message into the engine's store |
ingestBatch() |
Optional | Ingest a completed turn batch as one unit |
afterTurn() |
Optional | Post-turn lifecycle (persist context, trigger background compaction) |
assemble() |
Required | Core: assemble model context under a token budget |
compact() |
Required | Compress context (summaries, pruning, etc.) |
prepareSubagentSpawn() |
Optional | Prepare engine state before child agent starts |
onSubagentEnded() |
Optional | Notify engine that a subagent lifecycle ended |
dispose() |
Optional | Cleanup resources |
AssembleResult¶
type AssembleResult = {
messages: AgentMessage[]; // Ordered messages for model context
estimatedTokens: number; // Token estimate of assembled context
systemPromptAddition?: string; // Optional text prepended to system prompt
};
Registry and Resolution¶
- Engines are registered via
registerContextEngine(id, factory)(public SDK) orregisterContextEngineForOwner(id, factory, owner)(core) - Resolution order:
config.plugins.slots.contextEngine→ default slot ("legacy") - Only one engine active at a time (slot, not hook — exclusive)
- Registry is process-global (survives duplicated dist chunks)
LegacyContextEngine (Default)¶
The default engine (src/context-engine/legacy.ts) is essentially a no-op wrapper:
ingest()→ no-op (SessionManager handles persistence)assemble()→ pass-through (returns messages as-is, estimatedTokens: 0)afterTurn()→ no-opcompact()→ delegates to Pi's built-in compaction viadelegateCompactionToRuntime()
The real value is the interface: third-party plugins can replace LegacyContextEngine with custom implementations (RAG pipeline, vector store, graph-based context, etc.).
History Truncation¶
limitHistoryTurns() (src/agents/pi-embedded-runner/history.ts) provides pre-compaction truncation:
- Walks backwards through messages, counting user turns
- When count exceeds configured limit, slices from that point
- Limits are configurable per provider/channel/DM:
channels.<provider>.historyLimit— for channel/group sessionschannels.<provider>.dmHistoryLimit— for DM sessionschannels.<provider>.dms.<userId>.historyLimit— per-user DM override
This means long-running DM or channel sessions get automatic turn-based truncation even without compaction.
System Prompt Construction¶
OpenClaw's system prompt (src/agents/system-prompt.ts) is significantly more complex than Pi's ~300 words. It's built from 15+ sections:
- Identity — "You are a personal assistant running inside OpenClaw."
- Tooling — Dynamic tool list with descriptions (20+ possible tools)
- Tool Call Style — Narration guidelines
- Safety — Anthropic-inspired safety rules
- CLI Quick Reference — OpenClaw subcommand reference
- Skills — Available skills with mandatory scan-before-reply instructions
- Memory Recall — memory_search/memory_get instructions
- Authorized Senders — Owner identity (raw or hashed)
- Messaging — Cross-session messaging, sub-agent orchestration
- Reply Tags — Native reply/quote support
- Voice (TTS) — Text-to-speech hints
- Documentation — OpenClaw docs references
- Workspace — Working directory and sandbox info
- Sandbox — Container workspace guidance (if sandboxed)
- Self-Update — Gateway update instructions
- Model Aliases — Provider/model alias mappings
- Date & Time — User timezone
Three prompt modes:
- "full" — All sections (main agent)
- "minimal" — Reduced: Tooling + Workspace + Runtime only (subagents)
- "none" — Just identity line
Provider-Aware Context Sanitization¶
Unlike Pi, OpenClaw sanitizes messages per provider before sending:
validateGeminiTurns()— Enforces Gemini's alternating turn rulesvalidateAnthropicTurns()— Enforces Anthropic's turn rulessanitizeToolUseResultPairing()— Ensures every tool_result has a matching tool_usesanitizeSessionHistory()— Removes invalid entries, repairs broken state
These run as part of the pre-LLM pipeline, ensuring messages conform to each provider's API constraints.
Subagent Model¶
OpenClaw has a built-in, gateway-mediated subagent system (not an extension like Pi).
Spawn Flow¶
Main agent (sessionKey: agent:main:xxx)
│
├─ tool call: sessions_spawn({ task: "...", agentId: "worker" })
│ │
│ ├─ Generate childSessionKey: "agent:worker:subagent:<uuid>"
│ ├─ callGateway("sessions.patch") → create child session
│ ├─ buildSubagentSystemPrompt() → build minimal system prompt for child
│ ├─ contextEngine.prepareSubagentSpawn() → engine prepares child context state
│ ├─ callGateway("agent") → send task message to child
│ └─ registerSubagentRun() → track in subagent registry
│
├─ Child runs in fully independent session with own context
│
├─ Auto-announce: child pushes completion event to parent (push-based, no polling)
│
└─ Parent can also:
sessions_send() → send messages to child (steering)
sessions_history() → read child's conversation history
subagents(action=steer|kill) → intervene mid-run
Key Characteristics¶
- Gateway RPC isolation: Not process-level (like Pi's
spawn), but session-level via gateway - Bidirectional communication: Parent can steer/read child; child auto-announces completion
- Context engine awareness:
prepareSubagentSpawn()andonSubagentEnded()lifecycle hooks - Depth limits: Configurable max spawn depth prevents infinite nesting
- Modes:
"run"(one-shot, cleanup after) or"session"(persistent, thread-bound) - Attachments: Can pass files to subagents via base64-encoded attachments
- Sandbox inheritance: Child inherits or requires sandbox based on parent's status
Comparison: Pi vs OpenClaw¶
| Aspect | Pi | OpenClaw |
|---|---|---|
| Context accumulation | Infinite, full context sent every call | Infinite storage, but multi-stage pipeline before sending |
| Pre-send processing | None (optional transformContext hook, unused) |
sanitize → validate → truncate → assemble |
| Turn-based truncation | None | limitHistoryTurns() per channel/DM config |
| Token budgeting | None | assemble(tokenBudget) via ContextEngine |
| Provider awareness | None (same format for all) | Per-provider turn validation (Gemini, Anthropic) |
| Compaction | LLM summary, threshold-triggered | Same base (inherited from Pi), but engine can override |
| ContextEngine | N/A | Pluggable slot interface, 7 lifecycle methods |
| System prompt | ~300 words, dynamic tool/guideline sections | 15+ sections, full/minimal/none modes |
| Subagent | Extension (process isolation, one-way) | Built-in (gateway RPC, bidirectional, lifecycle hooks) |
| Subagent context | Fully isolated, only text result returns | Isolated sessions, but parent can read history/steer |
Design Philosophy¶
- Defense in depth: Three layers of context management (truncate → assemble → compact) vs Pi's single layer
- Pluggable over hardcoded: ContextEngine as a slot allows complete replacement of context strategy
- Provider-aware: Unlike Pi's universal approach, OpenClaw adapts messages to each LLM provider's constraints
- Gateway-mediated orchestration: Subagents communicate through the gateway, enabling bidirectional control and lifecycle management
- Configuration-driven: History limits, context engine selection, and subagent behavior are all configurable per session/channel/provider