flowctx · OpenClaw Context Engine

Memory should fade with distance, not snap off.

A stance: context isn't a "fill-then-truncate" buffer — it's memory with depth. Sharper up close, more distilled far away, yet never truly forgotten.

When the window overflows, the common move is to hard-truncate earlier turns — throwing away the working knowledge, and the escape routes, they carried. flowctx is an OpenClaw ContextEngine that makes a different trade-off: fidelity by distance to the current task — the farther away, the harder it's compressed; the closer, the more it's kept intact; whatever you're working on right now stays untouched. The originals remain retrievable on demand. So the prefix cache stays warm, reasoning doesn't slow down, and the task never drops a thread — all three at once, not one traded for another. Three tiers:

  • older historySummary compression — folded by the LLM into a layered handoff note that preserves failed approaches and key identifiers verbatim.
  • recent past turnsStructured compression (zero LLM) — deterministic, content-type-aware, byte-exact reversible.
  • current turnZero semantic loss — the raw content goes into the model as-is: no compression, no summary.
  • C3Reversible · Auditable Originals are stored under a SHA-256 content hash; the model retrieves them byte-exact via flowctx_retrieve(hash) — the compressed view is lossy, the original is not.
  • C5KV-Cache friendly The prefix stays byte-stable via compression idempotence (same content → same hash → same bytes across turns); summaries fire only rarely and never disturb the prefix.
  • C4Backgrounded · Non-blocking Summarization fires only past the threshold, runs on the primary model, fire-and-forget — the current turn never waits on it.
  • C2Append-only · Persistent Never rewrites the host session; compression happens only in the read-time assemble() projection — on disk, the uncompressed original is always the source of truth.
Stage 1 · Init

At the start — everything is raw, nothing compressed.

The session just began, with no history yet. Every turn, OpenClaw calls the engine's assemble(), and messages flow into the window passed through by reference — bytes unchanged, the provider's KV cache always hits, no LLM called. Right now there's only this one turn, window usage is low, nothing is compressed, and the right side is all original.

assemble · pass-through by reference
Prefix messages are passed through as-is by reference, bytes unchanged → the prefix cache keeps hitting. Lightweight, zero LLM.
Stage 2 · Structured compression (<100k)

History exists, but not full yet — past turns get structured compression.

Turns pile up and the assembled size rises to about 52k — still below the 100k gate, so no summary is needed yet. flowctx applies structured compression first: in the past turns that have left the current task, the oversized tool results (whole source files read, long logs run) are deterministically compressed by content type (zero LLM, byte-exact reversible); structurally similar middle turns are simply elided. The current turn stays raw throughout.

detectContentType · past-turn tool_result routing
Code folded to signatures / JSON to a structural skeleton / diff to hunks / CLI rules engine. The original is stored under sha256[:24] and replaced with a marker:
[flowctx: code compressed 8400→190 chars · flowctx_retrieve(hash="a91f…")]
tool_use blocks are always passed through as-is; only results are compressed.
Stage 3 · First leaf chunk (hits 100k)

The window is nearly full — compress in ordered segments, not all at once.

Structured compression alone still pushes usage toward the 100k gate. Only now does flowctx reach for summarization — and it does so sparingly: it won't compress the whole history in one shot, it just carves out the oldest segment and folds it into a leaf node (d0-1-3). The rest of the older history queues up, folded segment by segment as its turn comes; the nearer past turns are still only structurally compressed, not summarized. The folded segment leaves behind a handoff notefailed approaches and key identifiers, word for word, and the original can be pulled back by hash anytime.

leaf chunk d0-1-3 (depth 0) · background summary · real audit
caller=context-engine.turn.maintenance · input 12,875 → output 3,400 tok · $0.059
Each segment is at most leafChunkTokens, fires only past the gate — summary is the last resort.
Stage 4 · Multiple leaf chunks → condense

As turns grow — chunks fold one by one, then condense.

The conversation continues, and older history is folded into independent leaves one chunk after another (d0-1-3 · d0-4-6 · d0-7-16 …) — each chunk is its own block by topic, undiluted, so early topics never fade out. When same-level leaves reach condenseFanout, flowctx condenses them into a single depth-+1 parent node d1-1-31; the child leaves are masked and only this one block is injected — so the number of summary blocks stays bounded even in very long sessions. That's "progressive folding": from a single chunk, to many chunks, to condense.

leaf chunks accumulate → condensed node d1-1-31 (depth 1)
d1-1-31 · childIds=[d0-1-3 · d0-4-6 · d0-7-16 …]
In the real run only 4/40 points crossed 100k; each time one chunk folds and pulls it back — assembled size stays near the threshold.
Stage 5 · Steady state

Steady state: three tiers stable, nothing truly lost.

The system reaches a steady state, and the right side shows three clean tiers: older history converges into a bounded condense node · recent past turns are structurally compressed · the current turn is always raw, untouched. Originals, scratchpad, and every summary layer are all persisted to SQLite, retrievable byte-exact via flowctx_retrieve(hash / node), rebuilt after a restart with zero LLM calls. This layering holds prefix cache, reasoning performance, and task completion all at once.

Context Window flowctx
Window usage0%
Waiting for messages…
Summary DAG0 nodes
scroll down ↓