import { type PtyCensusSnapshot, type Logger } from './pty-census.js'; import { type SystemctlRunner } from './systemd-scope.js'; import type { FsWatcher, SessionRow } from './fs-watcher.js'; /** Reclaim an idle operator session when free RAM falls below this. Sits one * step above Task 600's MIN_ADMISSION_MB (4 GB) so reclaim precedes refusal: * recovering RAM is preferable to refusing work. A judgement call, not a * measurement. */ export declare const RECLAIM_AT_KB: number; /** Free RAM the slice's MemoryHigh leaves for everything else. Also absorbs * the baseline's own imprecision (see resolveNonSessionBaselineKb). */ export declare const THROTTLE_MARGIN_KB: number; /** Re-apply MemoryHigh only on a delta larger than this. Chosen to exceed the * baseline's imprecision so measurement noise cannot trigger a write. */ export declare const HYSTERESIS_KB: number; /** Floor for the slice bound on a small host. Mirrors SLICE_MIN_HIGH_BYTES in * packages/create-maxy-code/src/port-resolution.ts. */ export declare const SLICE_MIN_HIGH_KB: number; /** How long an operator session's transcript must have been static before it * is a reclaim candidate. Far below the 8.7 h floor of the measured idle * population, and long enough not to take a session an operator is mid-thought * on. A judgement call, not a measurement. */ export declare const IDLE_TTL_MS: number; /** RAM held by everything that is neither a session nor free: the co-resident * Neo4j JVMs, Ollama, the brand servers, edge/manager, the OS. * * TAKES THE SLICE'S anon, NOT ITS memory.current, and the distinction is the * whole correctness of this function. memory.current = anon + file + slab + …, * where `file` is page cache the kernel hands back under pressure — and * MemAvailable ALREADY counts that cache as available. Subtracting * memory.current from a MemAvailable-derived residual therefore subtracts the * slice's cache twice, UNDER-stating the baseline and so OVER-stating the * bound (high = total - baseline - margin). That is the unsafe direction: a * weaker throttle. Worse, it ratchets — slice cache growth raises sliceKb * while leaving sysAvailKb untouched, so the bound climbs monotonically back * toward the ~96%-of-RAM value this task exists to remove. anon has no such * overlap with MemAvailable, so none of that happens. * * Measured on the laptop, 2026-07-17 (slice: current 7.94 GB, anon 5.61 GB, * file 1.16 GB, slab 0.99 GB; box: total 62.5 GiB, avail 38.3 GiB): * from anon -> 18.97 GiB * from memory.current -> 16.8 GiB (2.2 GB low — exactly file + slab) * The task measured Neo4j's four JVMs plus Ollama at ~19 GB independently, so * the anon form reproduces a figure derived a different way. That agreement is * the evidence for this formula; the memory.current form disagreed with it by * the precise size of the double-count. * * Stable as sessions come and go: session anon growth consumes sysAvailKb and * raises sliceAnonKb by the same amount, so the baseline holds. Cache growth * moves neither. Only a real baseline change (a brand added, Ollama loading a * model) moves it — which is what should move it. * * Still an estimate: slab is charged to the slice but counted here as * non-session, because MemAvailable counts only the reclaimable part of it. * That over-states the baseline slightly, making the bound SMALLER — this time * genuinely the safe direction, and THROTTLE_MARGIN_KB absorbs it. The reclaim * trigger reads sysAvailKb alone and needs none of this arithmetic. */ export declare function resolveNonSessionBaselineKb(sysTotalKb: number, sysAvailKb: number, sliceAnonKb: number): number; /** The slice's soft ceiling: total RAM, less what non-session processes hold, * less a margin to leave free. Soft throughout — MemoryHigh throttles and * reclaims, never kills. * * BE HONEST ABOUT WHAT THIS IS. On the non-floored branch it reduces: * * high = T - max(0, T - A - anon) - M = A + anon - M * * sysTotalKb cancels. The bound is therefore NOT "a share of total RAM" — it * is "what the slice's unreclaimable footprint is now, plus what is genuinely * free, less the margin", and it is identical on a 62 GB box and a 200 GB box * with the same A and anon. That is correct (A already encodes T, and the * question is how far the slice may grow from where it is), but the * total-minus-baseline SHAPE reads as though total RAM drives the answer, and * it does not. The two-function form is kept because the baseline is a real * quantity worth logging on its own — it is what the operator needs to see to * understand WHY the bound is what it is — not because total RAM is an input * in any meaningful sense. * * Units: MemoryHigh acts on memory.current, while the baseline is derived from * anon. Deliberate, not an oversight — the bound must be reachable in * memory.current terms, but the HEADROOM it encodes is an anon question, since * only anon growth consumes MemAvailable. * * What the kernel reclaims when the bound is crossed, exactly. The excess is * (anon + file + slab) - (A + anon - M) = (file + slab) - (A - M) * and the reclaimable cache on hand is (file + slab). So WHILE A >= M the * excess is always fully coverable from cache: the slice's cache is reclaimed, * which is what MemoryHigh is for, and no session is throttled. Below A = M * the bound falls under anon and the kernel does throttle the cohort's anon — * correct, and the designed third tier (recover at 8 GB free, refuse at 4 GB, * throttle here), but not free, so the qualifier matters. */ export declare function resolveSliceMemoryHighKb(sysTotalKb: number, nonSessionKb: number, marginKb: number): number; /** True when the bound is worth writing: never applied, or moved by more than * HYSTERESIS_KB. The baseline is stable but not noise-free, and every * co-resident brand's daemon derives the SAME box-wide value, so without a * band four daemons would rewrite the same drop-in on every tick. */ export declare function shouldApplyMemoryHigh(desiredKb: number, currentKb: number | null): boolean; /** The idle-oldest operator session, or null. * * There is NO transcript check, and the inversion is the point. reaper.ts:63 * refuses to reap anything with a transcript on disk, because for a specialist * a transcript means real work was done. Here the transcript is the * PRECONDITION that makes stopping safe: it is the durable artifact, and * `--resume ` reconstitutes the session from it. Stopping an idle * operator session is not lossy — it trades RAM for resume latency. * * "Idle" here means status:'idle' — NOT PRODUCING OUTPUT. It does NOT mean * detached, and these must not be conflated: an operator may be attached and * watching a session this returns. Under pressure that session can still be * taken. No attachment signal is consulted; the one that exists (Task 1705's * stream-lifecycle) lives in the UI server process, not this one. */ export declare function selectVictim(rows: readonly SessionRow[], nowMs: number, idleTtlMs: number): SessionRow | null; export interface ReclaimLine { sessionId: string; /** The scope unit stopped, or 'none' when no tracker resolved. */ unit: string; idleMs: number; sysAvailKb: number; thresholdKb: number; /** Null renders as `unknown`, never 0 — see formatPtyCensus. */ sliceKb: number | null; } /** Pure — no I/O. Carries what happened AND the numbers that caused it, so an * auto-stop is as legible as an operator stop. */ export declare function formatReclaim(a: ReclaimLine): string; /** Pure — no I/O. Pressure with no reclaimable session is a real state and must * not be silent; mirrors `[pty-cap] ... evicted=none`. */ export declare function formatPressureNoVictim(sysAvailKb: number, thresholdKb: number): string; /** Pure — no I/O. Names the baseline so an operator can see WHY the bound is * what it is, not merely that it changed. */ export declare function formatSliceHighApplied(baselineKb: number, highKb: number, previousKb: number | null): string; export interface SliceMemoryPolicy { start(): void; stop(): void; /** Run one pass synchronously. Exposed for tests. */ tickOnce(): void; } export interface SliceMemoryPolicyDeps { watcher: FsWatcher; intervalMs: number; killGraceMs: number; logger: Logger; /** Injectable clock for tests. Defaults to Date.now. */ now?: () => number; /** Injectable measurement for tests. Defaults to the census collector, whose * reads this deliberately reuses rather than duplicating. */ collect?: () => PtyCensusSnapshot; /** Injectable systemctl for tests. Defaults to spawnSync. */ runSystemctl?: SystemctlRunner; } export declare function createSliceMemoryPolicy(deps: SliceMemoryPolicyDeps): SliceMemoryPolicy; //# sourceMappingURL=slice-memory-policy.d.ts.map