BotOS deployment:
join — cheap and lossless, with no user-visible impact.
MemoryPressurePolicy sheds new turns under pressure; this planner reclaims memory from idle warm caches. See Admission Control · Memory-aware admission for the sibling admission-side control.Two eviction layers, one planner
Two enactors share one decision core — the same pure planner and the same cgroup probe drive both. Layer 1 (existing) sheds warm per-session agent caches — the chat history plus agent object held byBotSessionManager. Layer 2 (new in PR #5237) sheds live WebSocketGateway session objects — the GatewaySession with its pending inbox, protocol cursors, executing-turn flag, and event log. Both call the same pure planner (plan_pressure_evictions) with WarmSession facts, and both read the same CgroupMemoryPressure probe. The decision is central; the enactors are layer-local.
Which layer sheds first? On a running gateway both sweeps run on the housekeeping tick — the warm-cache one from BotSessionManager (inside BotOS) and the gateway-session one from WebSocketGateway._liveness_loop. Together they cover both memory sources.
Quick Start
1
On by default on cgroup-aware Linux hosts
No knob to set. On a Linux host that can read a cgroup v1/v2 memory limit, the gateway samples RSS and soft-evicts the coldest rebuildable caches under pressure on its periodic tick. On a host that can’t report a cgroup limit (macOS/Windows, unconstrained host), the sweep is a no-op — legacy behaviour is preserved exactly.
2
Add a persistent store to make eviction rebuildable
A cache is only evicted when its transcript is durably persisted, so it can be losslessly rebuilt on the next turn. A For a full
BotSessionManager reads/writes that store via its store= argument. Without a store, nothing is flushed, nothing is evicted, and the sweep is a no-op.BotOS deployment, configure the store through gateway.yaml (session.persist: true) — see Gateway Session Persistence.3
Trigger the sweep yourself
Running the session manager standalone (no
BotOS)? Call the enactor directly with the concrete cgroup probe:Which primitive?
Two gateway primitives protect memory from opposite sides — this diagram picks the right one. Use both together for full memory protection: admission gates new work; the planner reclaims from idle warm caches.API Reference
Three symbols export frompraisonaiagents.gateway:
WarmSession
A frozen dataclass carrying a pure fact from the running gateway to the planner.
MemoryPressureProtocol
A @runtime_checkable Protocol the gateway implements over its own process to report the container’s memory ceiling and current RSS.
plan_pressure_evictions
session_ids to soft-evict, coldest (LRU) first, tie-broken by session_id (stable). It never touches the caches — the gateway is the enactor.
Returns
[] when RSS is within budget, the budget is unknown (None / <= 0), the budget or RSS is non-finite (NaN/inf), or nothing evictable remains.
Using CgroupMemoryPressure
CgroupMemoryPressure is the concrete, stdlib-only MemoryPressureProtocol the gateway uses to read the real cgroup budget and anonymous RSS.
A nested cgroup’s lower limit is preferred:
cgroup_limit_mb() resolves the process’s own cgroup path from /proc/self/cgroup first, then falls back to the filesystem root. Every source degrades safely, so on a host with no cgroup limit the sweep is a no-op.
Wrapper-side wiring
BotSessionManager is the runtime enactor — it snapshots warm caches, calls the pure planner, and soft-evicts the losers.
warm_sessions
WarmSession facts (LRU last_activity, in_flight, flushed). A cache is flushed only when a persistent store is configured and its last store write succeeded.
sweep_under_pressure
memory_pressure, calls plan_pressure_evictions, and soft-evicts the coldest rebuildable caches LRU-first — re-sampling RSS to shed the minimum. Returns the number evicted. A no-op (returns 0) when no cgroup limit is reported.
Live gateway sessions (WebSocketGateway._sweep_sessions_under_pressure)
The gateway’s own enactor sheds cold live GatewaySession objects before the kernel does — the second layer, new in PR #5237.
What it evicts. A GatewaySession in WebSocketGateway._sessions that is (a) not currently executing a turn, (b) has no live WebSocket bound to it, and (c) whose transcript is durably persisted. When shed it is closed with persist=True, dropped from the live set, and its resume_window TTL is stamped so the next join transparently rehydrates it from the store.
Guards — a session is skipped (never evicted) when:
Interaction with
close_session(persist=True). The sweep does not call close_session directly — it uses a stricter helper (_evict_persisted_session) that only drops the in-memory session after the final store.add_message write returns truthy (or None, for legacy stores that don’t report).
Headroom. The gateway sweeper uses _pressure_headroom = 0.9 (matching the warm-cache default). It sheds until RSS ≤ headroom × budget, re-sampling anonymous RSS after each eviction and stopping as soon as it fits — never more (test_gateway_sheds_coldest_session_first_until_within_budget, test_gateway_noop_when_within_budget, test_gateway_noop_when_no_sessions).
How it works — live sessions
The gateway builds a candidate list of disconnected sessions, asks the planner for the LRU order, then persists-then-drops each victim while re-sampling RSS. Theresume_window TTL stamped on each eviction is the persistence contract that keeps the session resumable — see test_gateway_evict_persisted_session_sets_resume_ttl.
Runs even when liveness is disabled
Since PR #5237, the housekeeping loop that drives this sweep no longer exits whenLivenessConfig(enabled=False) / interval_ms=0 — only the PING/reap logic is gated. When liveness is disabled the loop falls back to the config’s default interval so the pressure sweep still fires. This preserves the OOM guard as a hard invariant that does not depend on any other feature being enabled.
See Gateway Liveness for how the always-on housekeeping tick composes with the ping/reap logic.
How It Works
The gateway samples its own memory, asks the planner for the order to evict, then enacts it — re-sampling RSS as it goes. The planner returns the full ordered list, not a prefix — the gateway re-samples RSS as it evicts and stops as soon as RSS is back withinheadroom_ratio × budget (default 0.9). It is byte-agnostic by design: it never guesses per-cache sizes, so over-shedding is avoided by the enactor and under-shedding is caught on the next pass.
Guards — a victim is skipped entirely (never evicted) when:
Degraded modes — the planner never crashes the gateway it protects, returning
[] when:
Configuration
The single tunable isheadroom_ratio — the fraction of the budget the gateway sheds back down to.
User Interaction Flow
Warm agent caches (Layer 1)
A Fly machine with a 512 MiB cgroup limit hosts a Telegram bot that has accumulated 60 warm session caches. A burst pushes RSS to 490 MiB — over0.9 × 512 ≈ 460 MiB.
CgroupMemoryPressurereadscgroup_limit_mb() = 512,anon_rss_mb() = 490.plan_pressure_evictionsnames the 40 evictable sessions LRU-first (in-flight and unflushed ones are skipped).sweep_under_pressureevicts down the list, re-sampling RSS, and stops at ~460 MiB.- Those users get their cache rebuilt transparently on their next turn.
Live WebSocket sessions (Layer 2)
A gateway with a 512 MiB cgroup limit is serving 80 concurrent WebSocket clients. 25 of them idle out (no live socket bound; transcripts already persisted to the SQLite store from ordinary turn completions). A tool-heavy burst on the remaining 55 pushes RSS to 490 MiB — over0.9 × 512 ≈ 460 MiB.
- On the next housekeeping tick,
_sweep_sessions_under_pressure()readscgroup_limit_mb() = 512,anon_rss_mb() = 490. - It builds a
WarmSessionlist of the disconnected sessions only — bound sessions and executing turns are excluded. plan_pressure_evictionsreturns the 25 evictable session IDs LRU-first._evict_persisted_sessioncloses them one by one — each final snapshot written, then dropped from_sessions, with aresume_windowTTL stamped so they remain resumable.- It re-samples RSS after each eviction and stops when RSS is back inside the target, well before it exhausts the 25 candidates.
- When one of those users re-joins, the gateway rehydrates the session from the store transparently — no user-visible impact, no OOM kill, no dropped socket.
reap_stale; the new sweep sits alongside it and is a no-op when the host has no cgroup limit:
Common Patterns
Combine withMemoryPressurePolicy for full memory protection — admission gates new work, the planner reclaims from idle warm caches:
MemoryPressureProtocol for non-Linux hosts — supply a macOS/Windows probe:
headroom_ratio for hosts with spiky per-turn allocations — drop to 0.8 to leave more headroom:
Best Practices
Never re-implement the planner in an adapter
Never re-implement the planner in an adapter
Call
plan_pressure_evictions and enact the returned list. The decision is pure and unit-tested in isolation — a re-implementation drifts from the guards and degraded-mode returns.The planner is byte-agnostic — re-sample RSS as you evict
The planner is byte-agnostic — re-sample RSS as you evict
It returns the full LRU-ordered list, not a prefix, and never guesses per-cache sizes. Evict down the list and stop as soon as RSS is back within
headroom_ratio × budget. Never guess how much each cache will free.Never evict in_flight=True or flushed=False
Never evict in_flight=True or flushed=False
The planner already skips them — an executing turn would be aborted, and an unflushed transcript isn’t rebuildable yet. Do not override the guards in your enactor.
Trust the degraded-mode returns
Trust the degraded-mode returns
An empty list means “no work to do” — RSS within budget, unknown/
<= 0 budget, non-finite inputs, or nothing evictable. It never means the planner is broken; it never crashes the gateway it protects.Related
Admission Control · Memory-aware
The admission-side sibling — sheds new turns under RSS pressure with
MemoryPressurePolicyGateway Reliability Presets
One switch that composes the gateway’s protective primitives
Gateway Session Persistence
Why evicted caches can be losslessly rebuilt — an unflushed transcript is never evicted
Gateway Graceful Drain
The other “avoid dropping live sessions” primitive
Gateway Liveness
Housekeeping tick that drives the sweep — runs even when disabled
Gateway Session Continuity
What survives an eviction — the resume path
Core planner introduced in PraisonAI PR #3805 (fixes #3804). Wrapper-side wiring (
CgroupMemoryPressure, BotSessionManager.warm_sessions / sweep_under_pressure, per-turn in-flight and flushed guards) added in PR #4296 (fixes #4294). Live-session shedding (WebSocketGateway._sweep_sessions_under_pressure, bound-client guard, failed-persist retention, always-on housekeeping tick) added in PR #5237 (fixes #5153).
