mnesis.compaction.engine¶
engine
¶
Compaction orchestration engine with condensation and multi-round loop.
This engine implements the full Mnesis compaction flow:
- Tool output pruning — backward-scan and tombstone oversized tool outputs.
- Summarisation — level 1 → level 2 → level 3 escalation over the raw messages still in the context (never ones an earlier summary already covers). The recorded span is exactly what was summarised; input beyond the summariser's cap is left raw and handled by a further pass.
- Condensation — if the context (live summaries included) is still at or above half the soft threshold after summarisation, condense the summary nodes (level 1 → 2 → 3).
- Multi-round loop — repeat steps 2-3 up to
max_compaction_roundstimes until either the context fits or no progress is made.
Soft/hard threshold distinction:
- Soft (
soft_threshold_fraction* usable, default 60 %) — triggers early background compaction so the next turn is likely already compact. - Hard (100 % of usable) — blocks the next send until compaction finishes, preventing an over-limit context from reaching the LLM.
Both thresholds compare against the size of the current context window
(system prompt + live summaries + raw messages that the next LLM call would
carry), not lifetime billed tokens. The size shrinks as soon as compaction
swaps messages for a summary. The engine's own "does it fit" check uses the
same measure (see context_measure) and condenses summaries until the
context is below half the soft threshold, which makes compactions rarer than
stopping at the trigger (not guaranteed when the system prompt and recent turns
alone exceed that target).
File IDs are propagated through every compaction round; see
:mod:mnesis.compaction.file_ids and :mod:mnesis.compaction.levels.
CompactionTruncatedError
¶
Bases: Exception
The compaction model stopped at its output limit (finish_reason="length").
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. |