Skip to content

mnesis.compaction.engine

engine

Compaction orchestration engine with condensation and multi-round loop.

This engine implements the full Mnesis compaction flow:

  1. Tool output pruning — backward-scan and tombstone oversized tool outputs.
  2. 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.
  3. 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).
  4. Multi-round loop — repeat steps 2-3 up to max_compaction_rounds times 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 via check_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 should await 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

stalled: bool

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

more_to_compact: bool

True if the last run ended with work a further run could still do.

in_flight property

in_flight: bool

True while a compaction task (background or manual) is running.

has_pending property

has_pending: bool

True if a task handle is held, running or finished but not yet reaped.

note_user_turn

note_user_turn() -> None

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_context_measure(
    measure: Callable[[str], Awaitable[int]] | None,
) -> None

Set the callable that returns the session's current context size.

is_soft_overflow

is_soft_overflow(
    tokens: TokenUsage | int, model: ModelInfo
) -> bool

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 int, e.g. BuiltContext.context_tokens). A TokenUsage is also accepted and compared by its effective_total().

required
model ModelInfo

Model metadata providing context limit.

required

Returns:

Type Description
bool

True if tokens reach soft_threshold_fraction * usable.

is_hard_overflow

is_hard_overflow(
    tokens: TokenUsage | int, model: ModelInfo
) -> bool

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:is_soft_overflow).

required
model ModelInfo

Model metadata providing context limit.

required

Returns:

Type Description
bool

True if tokens reach the full usable budget.

is_overflow

is_overflow(
    tokens: TokenUsage | int, model: ModelInfo
) -> bool

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 force, run to the normal condensation target instead of stopping at the soft threshold.

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

wait_for_pending() -> CompactionResult | None

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 of the pending task (whether it was

CompactionResult | None

still running or had already finished), or None if no task was

CompactionResult | None

pending or it was cancelled. :meth:run_compaction never raises:

CompactionResult | None

a failed run is returned as a stub result with level_used == 0

CompactionResult | None

and an empty summary_message_id, so callers should check

CompactionResult | None

level_used before treating the result as a real summary.

compact_exclusive async

compact_exclusive(
    session_id: str, abort: Event | None = None
) -> CompactionResult

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.