mnesis.compaction¶
compaction
¶
Mnesis compaction components.
CompactionEngine
¶
CompactionEngine(
store: ImmutableStore,
dag_store: SummaryDAGStore,
token_estimator: TokenEstimator,
event_bus: EventBus,
config: MnesisConfig,
id_generator: Any = None,
session_model: str | None = None,
model_info: ModelInfo | None = None,
context_measure: Callable[[str], Awaitable[int]]
| None = None,
)
Orchestrates the full compaction protocol (summarise → condense → loop).
Guarantees:
- run_compaction() never raises on failure — errors are caught and Level 3
runs. Only external task cancellation (CancelledError) propagates.
- The resulting summary always fits within the token budget.
- Level 3 (deterministic) is the final fallback and always succeeds.
- Atomic SQLite commit per round: partial failures leave no inconsistent state.
- The EventBus receives COMPACTION_COMPLETED (or COMPACTION_FAILED).
Threshold semantics:
- Soft threshold (
soft_threshold_fraction, default 60 %): triggers early background compaction viacheck_and_trigger(). This keeps the context lean well before the hard limit. - Hard threshold (100 % of usable): checked inside
session.send()before the LLM call; if exceeded, the caller shouldawait wait_for_pending()to block until compaction completes.
Both thresholds take the size of the current context window in tokens
(see :attr:mnesis.context.builder.BuiltContext.context_tokens), never
lifetime usage. At most one compaction is in flight per engine:
:meth:check_and_trigger is a no-op while a previous task is running, and
:meth:compact_exclusive (manual compaction) waits for it first.
Example::
engine = CompactionEngine(store, dag_store, estimator, event_bus, config)
if engine.is_soft_overflow(tokens, model_info):
engine.check_and_trigger(session_id, tokens, model_info) # non-blocking
if engine.is_hard_overflow(tokens, model_info):
await engine.wait_for_pending() # block if must compact before LLM call
stalled
property
¶
True while background auto-compaction is paused as futile.
Set when a run ended at/above the soft threshold with nothing left to
summarise or condense; cleared by :meth:note_user_turn or by a run
that gets under the soft threshold. Never affects the blocking
hard-limit path.
more_to_compact
property
¶
True if the last run ended with work a further run could still do.
in_flight
property
¶
True while a compaction task (background or manual) is running.
has_pending
property
¶
True if a task handle is held, running or finished but not yet reaped.
note_user_turn
¶
Record that a new user turn was appended, re-arming a paused trigger.
A new user turn can make older messages summarisable (they leave the protected tail), so a pause based on the previous history no longer holds.
set_context_measure
¶
Set the callable that returns the session's current context size.
is_soft_overflow
¶
Return True if the soft threshold has been crossed.
The soft threshold triggers early background compaction (non-blocking).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tokens
|
TokenUsage | int
|
Size of the current context window in tokens (an |
required |
model
|
ModelInfo
|
Model metadata providing context limit. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if tokens reach |
is_hard_overflow
¶
Return True if the hard threshold has been crossed.
The hard threshold means the current context must be compacted before the next LLM call to avoid an over-limit request.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tokens
|
TokenUsage | int
|
Size of the current context window in tokens (see
:meth: |
required |
model
|
ModelInfo
|
Model metadata providing context limit. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if tokens reach the full usable budget. |
is_overflow
¶
Return True if compaction should be triggered (soft threshold check).
Preserved for backwards compatibility; delegates to
:meth:is_soft_overflow.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tokens
|
TokenUsage | int
|
Size of the current context window in tokens. |
required |
model
|
ModelInfo
|
Model metadata providing context limit. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if tokens exceed the soft threshold. |
check_and_trigger
¶
check_and_trigger(
session_id: str,
tokens: TokenUsage | int,
model: ModelInfo,
abort: Event | None = None,
force: bool = False,
full_drain: bool = False,
) -> bool
Check for overflow and trigger async compaction if needed.
Non-blocking — schedules a background task and returns immediately.
The task handle is stored in self._pending_task so callers can
await or cancel it during shutdown. At most one compaction is in
flight: while a previous task is still running this is a no-op, so
rapid overflowing turns cannot run concurrent compactions (duplicate
summaries / conflicting context-item swaps) or orphan the tracked task.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session_id
|
str
|
The session to compact. |
required |
tokens
|
TokenUsage | int
|
Size of the current context window in tokens. |
required |
model
|
ModelInfo
|
Model info for overflow detection. |
required |
abort
|
Event | None
|
Optional event to signal early termination. |
None
|
force
|
bool
|
The caller is about to block on the result (the hard-limit path): bypass the stall pause and stop the run once the context is under the soft threshold, leaving the rest of the drain to the next background run. |
False
|
full_drain
|
bool
|
With |
False
|
Returns:
| Type | Description |
|---|---|
bool
|
True if a new compaction task was scheduled, False if the context |
bool
|
is under the soft threshold, a compaction is already in flight, or |
bool
|
background compaction is paused (see below). |
A run that ends still at/above the soft threshold with nothing left to
summarise or condense (typically: the protected last two user turns
alone exceed it) cannot be improved by running again. Re-triggering on
every turn would burn LLM calls without progress, so the background
trigger is paused until a new user turn arrives
(:meth:note_user_turn), which can make older messages summarisable.
A warning is logged once when the engine pauses. The pause never applies
with force=True: the blocking hard-limit path always re-evaluates. A
futile run makes no summarisation calls; it can only repeat a condensation
for a set of live summaries that is new since the last condensation that
failed to shrink them even at the deterministic level.
wait_for_pending
async
¶
Await any in-flight background compaction task to completion, then release it.
The handle is released unless a newer run replaced it while waiting
(that run stays tracked; loop on :attr:has_pending to drain).
Cancelling the caller does not cancel the compaction: it is shielded, so
the run continues in the background, the handle is kept, and a later
call (or close()) collects it. asyncio.CancelledError still
propagates to the cancelled caller.
Returns:
| Name | Type | Description |
|---|---|---|
The |
CompactionResult | None
|
class: |
CompactionResult | None
|
still running or had already finished), or |
|
CompactionResult | None
|
pending or it was cancelled. :meth: |
|
CompactionResult | None
|
a failed run is returned as a stub result with |
|
CompactionResult | None
|
and an empty |
|
CompactionResult | None
|
|
compact_exclusive
async
¶
Run a compaction as the tracked in-flight task, after any pending one.
Used for manual compaction so that it never overlaps a background run
(duplicate summaries / conflicting context-item swaps). abort ends the
run early with the stub result, as for :meth:run_compaction.
run_compaction
async
¶
run_compaction(
session_id: str,
abort: Event | None = None,
model_override: str | None = None,
until_under_hard: bool = False,
) -> CompactionResult
Run the full compaction protocol. Never raises on failure.
Setting abort ends the run with the stub failure result
(level_used == 0) and a COMPACTION_FAILED event whose payload
has aborted=True. External task cancellation (Task.cancel()) is
different: it is not swallowed and asyncio.CancelledError propagates
to the caller.
Note
The two cases are told apart with Task.cancelling(). If the
calling task earlier swallowed a cancellation without calling
uncancel(), its stale count makes an abort propagate as a
cancellation instead of returning the stub. Mnesis always runs
compaction in a fresh task, so it cannot hit this; callers awaiting
run_compaction(abort=...) directly from a long-lived task should
uncancel() after swallowing a cancel.
Steps (per round, up to max_compaction_rounds):
1. Run tool output pruner (reduce input size first).
2. Summarise raw messages: level 1 → level 2 → level 3.
3. Condense accumulated summary nodes if still over budget: lvl 1→2→3.
4. If no progress was made, break early to avoid spinning.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session_id
|
str
|
The session to compact. |
required |
abort
|
Event | None
|
Optional asyncio.Event — checked before each level attempt. |
None
|
model_override
|
str | None
|
Override compaction model (for testing). |
None
|
until_under_hard
|
bool
|
Stop draining once the context is under the soft threshold (used when a caller blocks on the run) instead of continuing down to the condensation target. |
False
|
Returns:
| Type | Description |
|---|---|
CompactionResult
|
CompactionResult describing what happened. |
SummaryCandidate
dataclass
¶
SummaryCandidate(
text: str,
token_count: int,
span_start_message_id: str,
span_end_message_id: str,
compaction_level: int,
messages_covered: int,
)
A candidate compaction summary before it is committed to the store.
ToolOutputPruner
¶
Reclaims context space by tombstoning stale tool outputs.
The protect window ensures the most recent tool outputs (within the last
prune_protect_tokens worth of content) are never pruned. Only completed
tool calls outside this window are candidates.
No LLM call is required — this is entirely deterministic.
Example::
pruner = ToolOutputPruner(store, estimator, config)
result = await pruner.prune("sess_01JXYZ...")
print(f"Pruned {result.pruned_count} tool outputs ({result.pruned_tokens:,} tokens)")
prune
async
¶
Run a prune pass for the given session.
Algorithm: 1. Fetch all messages (including summaries) in chronological order. 2. Pre-fetch all raw parts to build a (message_id, tool_call_id) → part_id map. 3. Walk backward, tracking user turn count. 4. Stop at any is_summary message (compaction boundary). 5. Skip the most recent 2 user turns (protect recent content). 6. For each completed, non-protected, non-already-pruned tool part: - Accumulate output tokens. - Once outside the protect window (>40K tokens), add to candidates. 7. If total prunable volume < minimum threshold (20K): no-op. 8. Apply tombstones in a single batch UPDATE.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session_id
|
str
|
The session to prune. |
required |
Returns:
| Type | Description |
|---|---|
PruneResult
|
PruneResult with counts of pruned parts and tokens. |
level1_summarise
async
¶
level1_summarise(
messages: list[MessageWithParts],
model: str,
budget: ContextBudget,
estimator: TokenEstimator,
llm_call: Any,
compaction_prompt: str | None = None,
model_context_limit: int = 200000,
model_max_output_tokens: int = 0,
compaction_estimator: TokenEstimator | None = None,
) -> SummaryCandidate | None
Attempt Level 1 (selective) summarisation via LLM.
File IDs found in the input messages are automatically appended to the
summary via a [LCM File IDs: ...] footer.
Input messages are capped at 75 % of the compaction model's context window to prevent the summarisation call itself from overflowing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
messages
|
list[MessageWithParts]
|
All non-summary messages in the session. |
required |
model
|
str
|
Model string to use for compaction. |
required |
budget
|
ContextBudget
|
Token budget for validation. |
required |
estimator
|
TokenEstimator
|
Token estimator for result validation. |
required |
llm_call
|
Any
|
Async callable |
required |
compaction_prompt
|
str | None
|
Custom system prompt override. |
None
|
model_context_limit
|
int
|
Context window of the compaction model. |
200000
|
model_max_output_tokens
|
int
|
Output limit of the compaction model (0 = unknown). |
0
|
compaction_estimator
|
TokenEstimator | None
|
Estimator in the compaction model's units, used to size the input against its window (defaults to estimator). |
None
|
Returns:
| Type | Description |
|---|---|
SummaryCandidate | None
|
SummaryCandidate if successful and fits budget, or None to escalate. |
level2_summarise
async
¶
level2_summarise(
messages: list[MessageWithParts],
model: str,
budget: ContextBudget,
estimator: TokenEstimator,
llm_call: Any,
compaction_prompt: str | None = None,
model_context_limit: int = 200000,
model_max_output_tokens: int = 0,
compaction_estimator: TokenEstimator | None = None,
) -> SummaryCandidate | None
Attempt Level 2 (aggressive) summarisation via LLM.
Uses a more compressed prompt format and drops reasoning details. File IDs found in the input messages are propagated to the summary. Input is capped at 75 % of the compaction model's context window.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
messages
|
list[MessageWithParts]
|
All non-summary messages in the session. |
required |
model
|
str
|
Model string to use for compaction. |
required |
budget
|
ContextBudget
|
Token budget for validation. |
required |
estimator
|
TokenEstimator
|
Token estimator for result validation. |
required |
llm_call
|
Any
|
Async callable. |
required |
compaction_prompt
|
str | None
|
Custom system prompt override. |
None
|
model_context_limit
|
int
|
Context window of the compaction model. |
200000
|
model_max_output_tokens
|
int
|
Output limit of the compaction model (0 = unknown). |
0
|
compaction_estimator
|
TokenEstimator | None
|
Estimator in the compaction model's units, used to size the input against its window (defaults to estimator). |
None
|
Returns:
| Type | Description |
|---|---|
SummaryCandidate | None
|
SummaryCandidate if successful and fits budget, or None to escalate. |
level3_deterministic
¶
level3_deterministic(
messages: list[MessageWithParts],
budget: ContextBudget,
estimator: TokenEstimator,
) -> SummaryCandidate
Level 3 deterministic fallback (no LLM required).
Keeps the most recent messages that fit within 85% of the usable budget, prefixed with a truncation notice. This always produces a valid result.
File IDs found in all input messages (not just kept ones) are preserved
in a [LCM File IDs: ...] footer even when their surrounding context is
truncated — this is the lossless guarantee.
Precedence when the budget is tight: file IDs are reserved first, then
prose fills what remains, so messages are dropped before any file ID is.
Only if the header plus the ID footer cannot fit in 85% of
budget.usable is the footer truncated, keeping the most recently
referenced IDs (by last occurrence; the footer itself stays in
first-occurrence order). A warning with the dropped count is logged; the
raw files stay addressable in the immutable store. Truncating there is the
only way to keep the "always fits the budget" guarantee that lets
compaction make progress. If budget.usable is smaller than the minimal
header, no output can fit, so all IDs are kept (lossless wins).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
messages
|
list[MessageWithParts]
|
All non-summary messages in the session. |
required |
budget
|
ContextBudget
|
Token budget — result is guaranteed to fit within usable. |
required |
estimator
|
TokenEstimator
|
Token estimator. |
required |
Returns:
| Type | Description |
|---|---|
SummaryCandidate
|
SummaryCandidate that fits within budget.usable (barring a budget too |
SummaryCandidate
|
small for even the minimal header), as measured by estimator. |