Magic Context manages the context window of long-running coding-agent sessions. It keeps a session inside the model's window without losing its history, and it does so while keeping the provider's prompt cache stable, because on a long session the cached prefix is most of the cost.
It runs in several hosts and shares one implementation of the core behaviour:
- OpenCode 1.x and 2.x, as the npm plugin
@cortexkit/opencode-magic-context(packages/plugin). The 1.x and 2.x hosts have different plugin APIs; each has a thin adapter over the same core. - Pi and OMP, as
@cortexkit/pi-magic-context(packages/pi-plugin), which imports the core from the OpenCode package. - The Rust module
ck-mc(crates/), a module of the subconscious daemon (subc). It runs the transform and historian for sessions that live in a runner rather than a host with the plugin (mode 3, see "Where the transform runs"). In 0.49.x releases it also runs the experimental Rust transform mode and serves Claude Code through the Thalamus proxy.
All hosts and all projects on a machine share one SQLite database, ~/.local/share/cortexkit/magic-context/context.db. Session-scoped tables carry a harness column; project-scoped tables (memories, git commits) are shared across hosts. The Rust module keeps its own store, store.db, which moves in step with context.db (see Storage).
Paths below are relative to packages/plugin/ unless they name another package.
Magic Context rewrites the message array and system prompt the host is about to send, on every model call. The transform itself makes no model calls; summarisation and maintenance run in hidden child sessions.
- Tags and reduction. Every message part and tool output gets a
§N§tag. The agent marks what it no longer needs withctx_reduce; marked items are removed later, on a pass that is already rebuilding the cached prefix. A dropped text part becomes a[dropped §N§]placeholder. A dropped tool call either keeps its real arguments with a placeholder result (one of the newest 20 tool calls, with a small input) or is removed together with its result, leaving nothing (see Drop under Terms). Nothing is lost:ctx_expandrecovers any original from the raw history. - History. The historian reads older raw history in chunks and writes compartments: summaries of a stretch of work, each with four detail tiers, an importance score, facts and events. A deterministic renderer picks one tier per compartment from its age, importance and the current budget, so history degrades gracefully as it ages.
- The head of the prompt. Compartments, memories, project docs and the user profile are rendered into two synthetic user messages at the start of the conversation:
m[0], a frozen baseline, andm[1], the delta sincem[0]was last rebuilt. The protected section below defines exactly when each may change. - Knowledge. Project memories (
ctx_memory), notes and smart notes (ctx_note), unified search over memories, history, git commits and notes (ctx_search), and raw recovery (ctx_expand). - Maintenance. The dreamer runs scheduled background tasks: verifying memories against code, curating and classifying them, and the other tasks listed under Dreamer.
- Pressure. Reduction normally rides a bust that happens anyway. Near the limit a force band (about 85%) applies queued work and runs emergency reclaim; at a provider-proven 95% with nothing folded, the turn is refused rather than sent over the limit.
- Pass: one run of the transform. It runs once per model request, so once per step of a turn.
- Prefix: the part of the request the provider has cached, in request order: tool definitions, system prompt,
m[0],m[1], then the older messages. Changing any byte in it makes the provider read everything after that byte again at full price. - Rebuild (bust): a pass that changes bytes inside the cached prefix. Every reduction lands on a rebuild; appending at the end of the request is the only change a non-rebuilding pass makes. A bust has one of three levels, named by how far into the prefix it may change bytes: SOFT+ changes only the conversation after
m[1], SOFT also re-rendersm[1], and HARD re-rendersm[0](see Pass taxonomy). - Defer pass (cache hit): a pass that busts nothing. It replays every earlier decision byte for byte and only appends new messages.
- Rebuild permission: the single per-pass answer to "may this pass rebuild?". Every lane asks the same permission (invariant 4).
- Ride: a reduction landing on a pass that is already rebuilding for another reason, instead of causing a rebuild of its own.
- Lane: one kind of reduction, such as queued drops, routine heuristics, reasoning clearing, the emergency tool drop or a history refresh.
- Execute threshold: the context usage at which the scheduler says "execute" (
execute_threshold_percentage, default 65%, per model or as tokens). - Execute pass: a pass whose scheduler decision is "execute": usage is at or above the execute threshold, or the cache's idle lifetime has passed. For a subagent an execute pass is a rebuild permission. For a primary session it is one only when no summary can come (no historian run in progress or waiting to publish, and the historian not allowed to start) and either the queued
ctx_reducedrops are worth at least 2% of the usable window or this is the third execute pass in a row that held them (invariant 3). Otherwise its work rides the next bust. The scheduler's "defer" decision is not a defer pass: a pass the scheduler defers can still bust, for example when a fold is due. - Pressure episode: the run of passes from when usage reaches the execute threshold until it falls back below it.
- Force band: usage at or above
max(85%, execute threshold + 2). Queued work applies and the tiered emergency tool drop runs. - Emergency (95%): usage at or above 95%, or a provider overflow error. A primary session waits for its historian, and a request that still cannot fit is refused rather than sent.
- Tag: the number
§N§given to each message part.ctx_reducenames tags. - Drop: removing a tagged part's content. A text part becomes
[dropped §N§]. A tool call among the newest 20 with a small input keeps its real arguments and its result becomes[dropped §N§](attachments are stripped too); any other dropped tool call is removed together with its result. A call stays as such a skeleton when removing it is impossible: its result ends the request, the host cannot remove it, or it sits next to native reasoning on Anthropic. Duplicate tool outputs that heuristics drop are always removed whole. The choice is made once and replayed on every later pass (apply-operations.ts).ctx_expandbrings the original back. A queued drop is whatctx_reducerecords; it lands on the next rebuild. - Historian: the hidden child session that summarizes older raw history into compartments (summaries of a stretch of messages). Publication stores new compartments and changes nothing in the request by itself.
m[0]/m[1]: the two synthetic user messages at the start of the request:m[0]is the frozen baseline (docs, profile, memories, history), andm[1]is everything added sincem[0]was last rebuilt.- History refresh (SOFT): a rebuild that re-renders
m[1]with newly published summaries whilem[0]stays byte-identical. - Fold (HARD): a rebuild that re-renders
m[0]:m[1]merges into a new baseline, summaries are re-tiered by age, andm[1]resets. It happens when the provider cache is already lost (model or system-prompt change, idle expiry) orm[0]'s own content must change (see themustMaterializetriggers). - Cut: the point before which raw messages are no longer sent, because summaries in
m[0]/m[1]cover them. It moves on a history refresh or a fold, never on a defer pass. On OpenCode 1 a compaction marker in the host's store also stops the host loading those messages. - Primary session / subagent: a primary session is one a person works in (a top-level OpenCode or Pi session, or a head session hosted by a runner in mode 3; see Where the transform runs). A subagent is a session another agent started (an OpenCode child session, a mode-3 worker or reader).
- Signed thinking, prefix-bound model: Anthropic thinking blocks carry signatures bound to the bytes before them. On prefix-bound models (Fable 5.1, Opus 5.5, Sonnet 5.5) any edit before a kept block invalidates it. A hold is a pass that deliberately does not rebuild, to keep that thinking right after heavy reasoning.
| Layer | Where | Role |
|---|---|---|
| Entry | src/index.ts |
Load config, register hooks, tools, hidden agents, RPC server and dream timer. Default-exports both host shapes ({ id, server } for 1.x, setup for 2.x). |
| OpenCode 1 adapter | src/plugin/ |
Hook wrappers, tool registry, RPC handlers, dream-timer lifecycle, Rust-mode tool routing. |
| OpenCode 2 adapter | src/v2/ |
Context and compaction hooks, store reader, hidden-completion executor, fold ownership, TUI seam. |
| Runtime | src/hooks/magic-context/ |
The transform, its postprocess phase, history rendering, nudges, strip-and-replay, the Rust-mode adapter. |
| Services | src/features/magic-context/ |
Storage and migrations, tagger, scheduler, memory, dreamer, search and indexes, overflow detection. |
| Tools | src/tools/ |
ctx_reduce, ctx_expand, ctx_note, ctx_memory, ctx_search. |
| Shared | src/shared/, src/config/ |
Logger, SQLite backend, RPC, config schema and loader, prompt surfaces, window geometry. |
| Pi | packages/pi-plugin/src/ |
Pi and OMP hooks over the shared core. Deliberate differences are listed in packages/pi-plugin/PARITY.md. |
| Rust | crates/mc-core, mc-store, mc-module, mc-tokenizer |
Transform and classification, the module store, the subc module and historian, the token estimator. |
| CLI | packages/cli/ |
npx @cortexkit/magic-context setup, doctor and migrations. |
| Dashboard | packages/dashboard/ |
Tauri desktop app: config editor, sessions, cache diagnostics, memories. |
STRUCTURE.md has the directory map and where to add new code.
- In the host (OpenCode 1 and 2, Pi, OMP): the TypeScript transform in
transform.tsandtransform-postprocess-phase.ts. This is the reference implementation. Every other path must behave the way it does, lane for lane (see Session modes). - Rust transform mode (0.49.x releases only): with
transform_mode: "rust"(experimental; OpenCode 1 only, and only when subc is configured at user level; otherwise it falls back to TypeScript), the TypeScript layer becomes a coordinator: it syncs state to the module, sends each pass over subc, and serves the module's output. If the module fails, it replays the last known good output for the session (lkg-slot.ts,lkg-replay.ts) so the cached prefix survives the fault, and refuses the turn rather than send an unmanaged prompt when it cannot. 0.50.0 removes this mode. - Mode 3 (0.50.0 onward; not in 0.49.x releases): the session lives in an llm-runner instead of a host with the plugin. An llm-runner implements the
llm-runner/v1contract and can run any session shape, head or worker. THALAMUS is the llm-runner for every harness we don't maintain (OpenCode, Pi, Claude Code); Broca is the llm-runner we fully own and control. Which runner hosts which shape today is an operational limit, not part of the design. The runner reaches ck-mc only through the published contracts (llm-runner, compaction-provider, step-transform-provider): Setup and notices, views it fetches and applies,compaction.ride(the runner reporting that the provider cache is already lost, so held work lands in the rebuild that loss forces anyway), and thepre_userhook. All Magic Context policy lives in ck-mc; none runs in the runner. A session's preset decides its mode:headis a primary session,workerandreaderare subagents. ck-mc must implement the Session modes table lane for lane, with the TypeScript transform as its specification. A lane TypeScript has and ck-mc lacks is a bug, never a design choice. Open gaps are tracked in.cortexkit/internal-docs/investigations/mode3-parity.md.
This is the heart of the system and the part most easily gotten wrong. A transform pass is one decision about what the model sees on its next request: which reductions apply, what m[0]/m[1] hold, and whether the cached prefix may change. The rules below (taxonomy, gates, invariants) are the same for every implementation; how a pass runs differs:
- In a host with the plugin (OpenCode 1 and 2, Pi, OMP): the host hands the plugin the whole message array before every model request, so once per step of a turn (OpenCode:
experimental.chat.messages.transform, wrapped insrc/plugin/messages-transform.ts; Pi: thecontextevent). The pass rewrites that array in place. The host rebuilds the array from its own store each time, so every pass replays every earlier decision byte for byte and then decides whether anything new may apply. If the database is busy, the wrapper replays the last good request or refuses the turn; it never sends an unmanaged one. - Mode 3 (ck-mc behind an llm-runner): the runner holds the conversation and renders every request itself; ck-mc never receives or walks the whole array per step. It hears about new messages through notices, reads only messages it hasn't seen, keeps its own index of the rest, and publishes a view: edits keyed by message and part identity (the history head, drops, placeholders, replacements) that the runner applies to every request until a newer view replaces it. A pass runs on a Setup, a notice or a cache-loss ride, not on every step. A step that gets no new view is a defer pass by construction, because the runner re-applies the same view; a bust is a new view that changes bytes the previous one served.
- Resolve usage + scheduler decision (
executevsdefer). - Emergency overflow recovery if ≥95%.
- Compartment trigger check (off the in-memory
args.messagestail — noopencode.dbread steady-state); fire the historian async if eligible. - Prepare compartment injection (decide m[0]/m[1] materialization).
- Tag messages; replay dropped-status, caveman, reasoning, placeholder, image strips.
- Compartment phase: inject the
<session-history>(m[0]/m[1]) intomessage[0]. - Postprocess (
transform-postprocess-phase.ts): the mutation gates — pending-op drain, heuristic cleanup, nudges, synthetic-todowrite, auto-search.
SOFT+, SOFT and HARD are the three levels of cache bust; each name says how far into the cached prefix the pass may change bytes. Every reduction lane with work lands on a bust of any level; the levels differ only in what changes besides the conversation.
- Defer (
cache_hit): no bust. m[0], m[1] and every earlier message replay byte-identical; earlier drops and strips replay, nothing new applies, and only new messages are appended. The steady state — most passes are this. - SOFT+ (conversation bust): m[0] AND m[1] replay byte-identical, so
tools + system + m[0] + m[1]stays cached and the cache busts at the first changed message after them. New reductions land in the conversation: queuedctx_reducedrops, heuristic drops, smart drops (tool output a later call superseded, whensmart_dropsis on), reasoning clearing, the force-band emergency drop. Examples: invariant 3's execute pass, a force-band or/ctx-flushpass when m[1] has nothing new, every subagent execute pass. - SOFT (m[1] bust): m[1] re-renders (new compartments / memories / user-profile surface as deltas) while m[0] stays byte-identical, and every SOFT+ reduction lands with it.
tools + system + m[0]stays cached; the cache busts at the m[1] breakpoint. Driven by an execute pass,/ctx-flush, or a pass that consumes a history publication held since an earlier pass, when m[1] has new content. - HARD (m[0] fold):
mustMaterializefires → m[0] re-materializes, folding m[1] into the new decayed baseline and resetting m[1] to a placeholder. The whole prefix rebuilds — but "for free" because the provider cache key was already dead (see HARD triggers in "m[0]/m[1] cache layout"). Decay re-tiering happens ONLY on a HARD fold — a SOFT pass must never re-tier (that would change m[0] bytes).
Pending-op drain and heuristic cleanup are each gated by the same shape in transform-postprocess-phase.ts:
shouldApplyPendingOps / shouldRunHeuristics =
(execute || materializationRequested || forceMaterialization || foldExecutedThisPass) // BUST clause: did a HARD fold land this pass?
&& (!compartmentRunning || emergencyBypassCompartmentGate) // VETO clause: is the historian mid-run?
- BUST clause — only mutate (drop tools, run heuristics) on a pass that is already busting the prefix, so the mutation rides that one bust instead of causing its own.
foldExecutedThisPassis true only after off-wire fold pre-execution reports that m[0] actually materialized; amustMaterializeadvisory by itself never opens mutation gates. The one pass that may open the gate without an independent bust is a primary execute pass under invariant 3's bounded queue exception. - VETO clause —
compartmentRunning— block mutation while the historian is summarizing the tail, so we don't change the bytes it's reading mid-run. Bypassed byemergencyBypassCompartmentGate, and by an explicit/ctx-flush, a deliberate user action whose drain is safe per the disjoint-DB model below. emergencyBypassCompartmentGatebypasses the veto whenforceMaterialization(≥85%) ORfoldExecutedThisPass— i.e. a hard fold drains pending ops + runs heuristics even while the historian runs, because the prefix is busting regardless (see "drain into the known bust" invariant). This is safe per the disjoint-DB model below; both harness twins use the shared executed-fold predicate.
- A HARD bust means the prefix is already gone → drain EVERYTHING into it. Never "defer" a hard bust. This pass IS the fold; there is no later fold to wait for. Deferring the drain only produces a second, avoidable bust ~one turn later. (The
compartmentRunningveto must therefore yield to a hard fold — the fold-exec bypass.) - A defer pass must replay byte-identical. Any first-application of a strip/drop on a defer pass changes tail bytes and busts the whole prefix after it. Watermark-gated strips (placeholders, images, stale-
ctx_reduce) use a frozen-id replay pattern: detect-and-freeze the affected ids only on cache-busting passes, replay the frozen set on every pass. There is exactly ONE drop placeholder string,[dropped §N§], a pure function of tag id — never re-derive bytes from mutated content (that caused repeated cache catastrophes). - Deferred work rides the next bust cycle; it forces its own only through one bounded exception. Historian publishes, compaction-marker moves, and queued drops accumulate while m[1] replays frozen, and materialize together on the next genuine bust (hard fold / flush / force band). A historian publish does NOT bust the cache — between busts every pass is
cache_hit. The exception: on a primary execute pass where no summary can come (none running, none waiting to publish, the summarizer not allowed to fire, no floor mark pending), the execute pass does its rebuild itself instead of waiting for a summary, provided enough queued work exists to justify it — queuedctx_reducedrops worth at least 2% of the usable window, or the third consecutive held execute pass — and that pass applies them and the usual cleanup in one rebuild.ctx_reduceitself never starts a rebuild: pressure causes the execute pass, and the drops only land in it. While a summary can still come, the drops hold for it. When the summarizer is blocked only by the newest real user prompt, a one-pass mark lets the next pass summarize past that prompt once, and the held drops land with that summary's rebuild. - Automatic reclaim is ride-only, and every lane shares ONE bust permission. Age sweeps, heuristic cleanup, supersession and duplicate dedup never originate a bust: they land only on a pass that is already busting for another reason (a fold or refold, a published-history refresh into m[1],
/ctx-flush, or the ≥85% force band). Queued agentctx_reducedrops ride that same permission: marking a message queues it, and the drop lands on the next bust cycle. They never start a rebuild themselves; besides riding one, they land on invariant 3's execute pass, whose queued-work bound counts only what the drain can actually remove, so undroppable or protected work can never make that pass rebuild. A subagent has no summarizer, so for it every execute pass is itself a rebuild permission, and every lane with work rides it: queuedctx_reducedrops, routine heuristics, reasoning clearing beyondkeep_reasoning_tokens, and from the force band the tiered emergency tool drop. Those lanes are all a subagent has, so a path that skips one of them lets the subagent grow until the window ends it. A single per-pass permission decides whether the pass busts, and every mutation lane — reductions, m[1] refresh, heuristics, synthetic todo, sentinel first-application — consults that same permission; a veto that applies to one lane and not another is a defect (the 2026-09-07 split bust on a head session: the age sweep bypassed the historian veto that held the m[1] refresh, so one threshold crossing became two priced busts). There is no mid-turn deferral: a tool loop is not a reason to hold an execute (the OpenCode detector never engaged and Pi's only measurable effect was withholding drops for hours on steered marathons), and Anthropic's incremental tool-loop caching makes a held mutation cost strictly more than one applied at first eligibility. The one exception is the bounded signed-thinking hold on prefix-bound thinking models (signed-thinking-hold.ts): right after heavy reasoning, and never at or above the execute threshold plus 10 points.
Mutating while the historian runs is safe because the two databases are disjoint on the read/write side:
- The historian reads raw OpenCode messages from
opencode.db(read-only) for its chunk. - Drops + heuristics mutate
context.db(tags/pending_ops) and the in-memory outgoing wire only. - The historian's in-flight snapshot is validated by
computeRawRangeFingerprint, which hashes raw content only (ids, part types, content lengths) — never tag/drop state — so a concurrent drop can't invalidate it. - Its post-publish
queueDropsForCompartmentalizedMessagesis idempotent against already-dropped tags.
The compacted history renders into TWO synthetic user-role message slots at the head, so the large stable prefix survives steady-state work. inject-compartments.ts (renderM0 / renderM1 / materializeM0 / mustMaterialize), mirrored in inject-compartments-pi.ts. Both slots prepend with synthetic: true parts so they don't count toward OpenCode's title-generation gate.
- m[0] — cumulative baseline (frozen, like
system[0]). Holds<project-docs>(rootARCHITECTURE.md+STRUCTURE.md), baseline<user-profile>, and the decay-rendered compartment history as of the last materialization. Does NOT change on routine turns. - m[1] — volatile delta. Holds everything added since the last m[0] materialization: new user-profile additions, new memories (via the
maxMemoryIdwatermark),<memory-updates>supersede deltas, and the newest compartments at full tier. Renders a minimal placeholder when empty (never fully empty — Anthropic cache-breakpoint structure).
mustMaterialize (HARD fold) triggers — organized around the bust taxonomy so the trigger list and the m[0]/m[1] contract can never silently disagree:
- Provider-side cache eviction (the cache is already dead, so folding is free): model/provider change (
cachedM0ModelKey), system-prompt-hash change (cachedM0SystemHash), idle > TTL (cacheExpired, self-consuming vialastResponseTime > cachedM0MaterializedAt), and on OpenCode 2 only, a change in the names or descriptions of the tools the session's own request carries (the tool list precedes the messages, so the provider cache is already lost). The fingerprint is adopted silently on a session's first pass after upgrade, schema-only differences never count, and a plugin that changes tools after Magic Context's hook is not seen (accepted limit). - Genuine m[0] content change (baseline bytes differ): first render,
cached_m1_missing,project_memory_epochchange (dashboard / external mutation), pending m[0] mutations (max_mutation_id— structural compartment delete/merge/recomp), upgrade-state change. - Deliberately NOT triggers (these are m[1] deltas — triggering would bust m[0] on routine background work and defeat the design): new compartment sequence,
project_user_profile_version,maxMemoryId, project-docs-hash change (docs edits fold in on the next natural hard bust, never on their own), and the process-global tool-set hash on OpenCode 1 and Pi (process-wide, false positives). - Pressure backstop refold: on a cache-busting pass, if no natural HARD bust has arrived but m[1] has grown large — gated by the m[1]/m[0] size ratio (with a small-m[0] floor) OR an absolute m[1] token cap (~20% of history budget) OR a large memory-mutation count.
applyMarkersToStateupdates ALLstate.cachedM0*fields post-materialize (guards against an infinite re-materialize loop)./ctx-flushis SOFT when m[1] has new content and SOFT+ otherwise (it drives the m[1] refresh and heuristics, never an m[0] fold).
Memory mutations route through m[1], not the epoch. In-session ctx_memory mutations do NOT bump project_memory_epoch: additive writes surface via the maxMemoryId watermark; non-additive (update/archive/merge) record a memory_mutation_log row rendered as a <memory-updates> delta. Both reconcile into m[0] on the next natural hard bust. The epoch is bumped only by dashboard mutations (an external editor can't otherwise signal a running session).
- Trigger (
compartment-trigger.ts): fires on pressure relative to the execute threshold, on commit clusters, and on the size of the unsummarised tail, reading only the in-memory message tail. - Boundary (
protected-tail-boundary.ts): decides which prefix of the raw history is eligible, keeping the live tail and never splitting a tool call from its result. - Produce (
compartment-runner-incremental.ts): runs the historian model on the chunk with a bounded prompt (seed examples, recent compartments, the memory block) and checks the prompt fits the model's window before sending. - Validate and publish: ordinals must be increasing and non-overlapping; the last compartment is discarded when it has no lookahead and re-read next time. Publishing stores compartments, promotes facts to memories, and queues drops for the summarised messages. It never forces a bust; the new history appears on the next pass that rebuilds the prefix.
- Render (
decay-render.ts,decay-curve.ts): one tier per compartment, from age, importance and budget pressure.
/ctx-recomp rebuilds compartments from raw history; /ctx-wrapup compacts older history on demand while keeping the newest messages raw.
Memories are project-scoped facts in five categories (project rules, architecture, constraints, config values, naming). They are stored with full-text and vector indexes; embeddings are plain SQLite blobs compared in memory. Agents write through ctx_memory; additions reach the prompt through m[1] and fold into m[0] on the next natural rebuild. Workspaces let projects share selected categories read-only.
ctx_search queries memories, the raw-message index, indexed git commits, compartment summaries and notes in one call, with optional date bounds, and hides results the session can already see.
A process-wide timer checks each task's cron schedule and runs due tasks in their own child sessions, one at a time per conflict domain (all memory-changing tasks share one lease). Tasks: map memories to the files that back them, verify them against code, curate, classify, learn from user friction (retrospective), maintain docs, promote and refresh primers, evaluate smart notes, review user memories, compress memory cues for the mural (the mural itself is rendered on demand, not by a dreamer task). Background writes never force a cache bust; they appear on the next natural rebuild. A failed task retries; a run's failure class and provider error are recorded and shown in /ctx-status.
- Schema migrations live in
src/features/magic-context/migrations.ts.LATEST_SUPPORTED_VERSIONinstorage-db.tsis the schema fence and must equal the newest migration; a binary whose fence is below the database refuses to open it. - Because every host shares
context.db, a migration lands together with a rebuild of both plugin dists and a restart of every host (bun run build:dists). A host left on an older fence fails closed. - New session-scoped tables go in
SESSION_SCOPED_TABLES(storage-session-tables.ts) so session deletion and the orphan sweep clean them. - SQLite parameters are bound as spread arguments, never an array (
node:sqlitereads an array as named parameters). context.dband the Ruststore.dbform one consistency unit; restore both from the same backup or neither.
The TypeScript transform decides a session's mode from sessionMeta.isSubagent (fullFeatureMode is its inverse) and from compaction.enabled. In mode 3 the preset decides it (see Where the transform runs).
Every lane lands only on a pass with rebuild permission; the first row says where that permission comes from, and the primary and subagent columns differ mainly there. Every rebuild means the lane applies on each such pass. Once per pressure episode means a primary session applies it on its first rebuild after usage reaches the execute threshold, and after that only on a rebuild that is already paying for itself: queued drops landed, a fold or history refresh, /ctx-flush, or 95%.
| Lane | Primary session | Subagent | Compaction off |
|---|---|---|---|
Tags, §N§ prefixes, ctx_reduce |
when the tool is callable | when the tool is callable | no |
| Rebuild permission comes from | a bust that happens anyway (fold, history refresh, /ctx-flush, force band, idle expiry), or its own execute pass when no summary can come and enough drops are queued (see Execute pass in Terms) |
every execute pass | nowhere |
Queued ctx_reduce drops land |
every rebuild | every rebuild | no |
Routine heuristics: dedup, supersession, injection strip (applyHeuristicCleanup) |
once per pressure episode | every rebuild | no |
Reasoning clearing beyond keep_reasoning_tokens ([cleared] on canonical Anthropic; removal for other providers and for prefix-bound models, oldest contiguous run only) |
once per pressure episode, with the routine heuristics | every rebuild | no |
| Tiered emergency tool drop | from the force band | from the force band | no |
| Refusal of a request proven over the window | yes | yes | no |
Historian, summaries, m[0]/m[1] history, folds, force materialization, overflow-recovery latch |
yes | no | no |
Memories, docs and user profile in m[0]/m[1] |
yes | no | yes, additive only |
| Caveman compression, auto-search, note nudges | yes | no | no |
ctx_reduce nudges, time markers |
yes | yes | no |
Every kind of session shrinks the same way: on a pass with rebuild permission, every lane that has work lands in that one rebuild. Two things differ. The first is where the permission comes from: a primary session rebuilds on a bust that happens anyway, or on an execute pass of its own when no summary can come and enough drops are queued (see Execute pass in Terms); a subagent rebuilds on every execute pass. The second is which lanes exist: a primary session also has its historian, whose cut removes every summarized message once a history refresh or fold renders the summary. A subagent has no historian, so the lanes marked yes for it are all it has. On reasoning-heavy models most of a long subagent's context is its own past reasoning, which only reasoning clearing removes.
Compaction off (compaction.enabled: false, user config, restart required) keeps memories, docs, notes, search and expand, and leaves context management to the host.
- Fail closed on storage. If storage cannot be opened or migrated, the transform refuses loudly on every pass instead of letting the prompt grow unmanaged (
fail_closed_blocking). - Fail open per turn. Ordinary per-turn handlers log and continue. A busy database (
SQLITE_BUSY) never lets a turn through unmanaged while compaction is on: the turn being sent retries the lock briefly, then replays the last good request, and otherwise refuses the turn. Background work tries once and picks up again on its next run. - Provider limits. Context-overflow errors are parsed and the learned limit is persisted for later passes.
- Hidden agents have step caps and are aborted on timeout.
Subsystem detail lives in docs/architecture/: the Rust module and its protocol, the OpenCode 2 adapter, Pi specifics, tagging and reclaim lanes, nudges, calibration, embeddings, the dreamer's tasks, storage and migration history.