/** * Adaptive agent limits. Normal mode: no cap, idle agents stop only when * memory is tight (IdleDetectionService). Pressure mode (memory/swap/OS * pressure or sustained high CPU load): cap running agents, stop idle ones * sooner, and queue starts that would exceed the cap. * * @module services/agent/resource-mode.service */ export declare const RESOURCE_MODE_CONSTANTS: { readonly DEFAULT_MAX_RUNNING_AGENTS: 6; readonly DEFAULT_IDLE_TIMEOUT_MINUTES: 10; /** Consecutive tight samples before entering pressure mode */ readonly ENTER_AFTER: 2; /** Consecutive calm samples before leaving pressure mode */ readonly LEAVE_AFTER: 3; /** Load average (5 min) above this many times the core count counts as tight */ readonly LOAD_PER_CORE: 2; readonly SAMPLE_INTERVAL_MS: 30000; /** How long a queued start waits for a slot before giving up (messages stay queued) */ readonly START_WAIT_MS: number; /** * A start that has waited this long for a slot stops the best available * agent even if it is idle for less than the idle timeout or has work in * flight: a waiting start often carries an owner message and must never be * blocked forever. */ readonly RELAX_AFTER_MS: number; /** * An agent started less than this long ago is never stopped to free a slot * (except at critical memory). Starting an agent and stopping it minutes * later wastes the whole start and releases its work back to the pool * (2026-10-16: Luna stopped 8 times in 2 h, some after 0-3 min idle). */ readonly MIN_RUN_BEFORE_EVICT_MS: number; /** * Hysteresis: for this long after A was stopped to start B, A's own start * may not stop B (2026-10-16: Luna and Max evicted each other back and forth). * Longer than MIN_RUN_BEFORE_EVICT_MS so it still holds once B's own start * protection has run out. */ readonly DISPLACED_GUARD_MS: number; /** * An agent idle for less than this is never stopped to free a slot — not * for an owner-message waiter, not when the rules relax — unless memory is * critical. Eviction at `idleMinutes: 0` (32 slot stops in 9 h on * 2026-10-16, Luna and Max taking turns) throws away an agent that just * finished a turn and is about to get the next one (crewly#1344). */ readonly MIN_IDLE_BEFORE_EVICT_MS: number; /** * The soft reasons to keep an agent running for a slot — the owner spoke * to it lately (answered), it stated an intent, or it waits on a teammate's * ticket or delegated work — lapse once it has been idle this long. Such an * agent is woken again by the message it waits for; holding the slot for * hours starved everyone else (2026-10-19: Ella waited 2 h 38 min, with * 1132 "owes the owner" keeps in a day). An unanswered owner message, an * owner promise and work in hand still protect at any idle. */ readonly SOFT_KEEP_MAX_IDLE_MS: number; /** A wait record older than this since its last touch is a new wait */ readonly WAIT_RECORD_TTL_MS: number; /** A started agent counts against the cap even before it shows as running */ readonly ADMIT_TTL_MS: number; /** errorCode of a start that waited for a slot and got none (deferred, not failed) */ readonly START_DEFERRED_ERROR_CODE: "RESOURCE_PRESSURE_CAP"; }; export type ResourceMode = 'normal' | 'pressure'; export interface MemoryReading { usedPercent: number; freeMB: number; totalMB: number; swapUsedPercent?: number; pressureElevated?: boolean; } /** * Whether memory alone is tight (used share, free floor, swap, OS pressure). * * @param stats - Memory reading * @returns True when tight */ export declare function memoryIsTightFor(stats: MemoryReading): boolean; /** A running, non-exempt agent as seen by the cap. */ export interface RunningAgent { sessionName: string; role: string; idleMs: number; /** Mid-turn (working status in_progress) */ busy: boolean; } export interface ResourceModeDeps { listRunning: () => Promise; stopAgent: (sessionName: string, role: string) => Promise; hasOwnerMessage: (sessionName: string) => boolean; limits: () => Promise<{ maxRunning: number; idleTimeoutMinutes: number; }>; /** * The agent owes the owner: an unanswered owner message, a promise in an * owner thread, or a stated "starting now". Such an agent is never stopped * to free a slot unless memory is critical; the waiter waits for another * victim (2026-10-08: Atlas and Ella were stopped mid-conversation; * 2026-10-12: Pia was stopped with a video owed). `idleMs` lets the soft * reasons lapse (see SOFT_KEEP_MAX_IDLE_MS). */ owesOwnerThread?: (sessionName: string, idleMs: number) => boolean; /** * Why this agent must not be stopped for a slot: it has work queued for it, * owns an open ticket, or delegated work that is still open (its report is * still to come). Null/undefined = free to stop. Errors count as free. * `idleMs` lets the waiting-on-a-teammate reasons lapse (see SOFT_KEEP_MAX_IDLE_MS). */ protectedReason?: (sessionName: string, idleMs: number) => Promise; /** * Whether the waiter is waiting on the candidate's work (it delegated an * open work item or ticket to it). Such a candidate is never stopped for * that waiter. Errors count as false. */ waitsOn?: (waiter: string, candidate: string) => Promise; /** An agent was stopped to free a slot (the owner thread it owes is told) */ onStoppedForSlot?: (sessionName: string) => void; /** A start waited for a slot and got none (deferred; its messages stay queued) */ onStartDeferred?: (sessionName: string) => void; } export declare class ResourceModeService { private static instance; private logger; private mode; private tightStreak; private calmStreak; private lastReason; private timer; private deps; private waiters; private seq; private admitted; /** The start promise of each agent waiting for a slot (one per agent) */ private pendingStarts; private pumping; /** When the "no slot" reason was last logged per waiter (the pump retries every 10 s) */ private lastNoSlotLog; /** When each agent's start first began waiting for a slot (survives the 2-minute deferral, cleared on admission) */ private waitingSince; private lastRunning; /** When each agent was last seen turning idle (events), or the first pump that saw it idle after seeing it busy */ private idleSince; /** Agents seen busy by a pump (their idle time starts at that sighting at the earliest) */ private lastSeenBusy; private lastCap; /** When each agent's start was last allowed (it counts as freshly started for MIN_RUN_BEFORE_EVICT_MS) */ private startedAt; /** Who displaced each agent stopped for a slot, and when (hysteresis, DISPLACED_GUARD_MS) */ private displacedBy; /** Readings, overridable in tests. */ memoryStats: () => MemoryReading; loadAvg: () => number; cpuCount: () => number; private constructor(); static getInstance(): ResourceModeService; static resetInstance(): void; /** * An agent's working status changed. Idle time for eviction is counted * from the busy to idle transition, not from its last output: an agent * that was busy for an hour with a silent background task is not "idle for * 60 minutes" the moment it finishes (Pia, 2026-10-12). * * @param sessionName - Agent * @param idle - True on busy to idle, false on idle to busy * @param at - Epoch ms of the transition */ noteWorkingStatus(sessionName: string, idle: boolean, at?: number): void; /** Idle time of a running agent counted from when it became idle. */ private withHonestIdle; /** Whether memory is critical: the only case where an agent that owes the owner may be stopped for a slot. */ private memoryCritical; setDeps(deps: ResourceModeDeps | null): void; /** Start periodic sampling. */ start(): void; stop(): void; getMode(): ResourceMode; /** Why a sample is tight, or '' when calm. */ private tightReason; /** * Take one reading and apply hysteresis: enter pressure after * ENTER_AFTER tight samples in a row, leave after LEAVE_AFTER calm ones. * * @returns The mode after this sample */ sample(): Promise; /** * Idle timeout in minutes for the current mode. * * @param normalMinutes - `general.agentIdleTimeoutMinutes` * @param pressureMinutes - `general.pressureIdleTimeoutMinutes` * @returns Effective timeout (0 = disabled) */ effectiveIdleTimeoutMinutes(normalMinutes: number, pressureMinutes: number): number; /** Snapshot for /health. */ stats(): { resourceMode: ResourceMode; running: number; cap: number | null; waiting: number; reason: string; }; /** * Ask for permission to start an agent. Resolves true at once in normal * mode. In pressure mode with the cap reached, tries to free a slot by * stopping the longest-idle agent; otherwise waits (owner-triggered starts * first). Resolves false when no slot opened in time — the caller leaves * the agent's messages queued. * * @param sessionName - Agent to start * @param ownerTriggered - An owner message is waiting for it * @returns True when the start may proceed */ requestStart(sessionName: string, ownerTriggered: boolean): Promise; /** * Whether a start for this agent is waiting for a slot right now. * * @param sessionName - Agent session * @returns True while it waits behind the running-agent cap */ isStartWaiting(sessionName: string): boolean; /** Queue a start behind the cap (see {@link requestStart}). */ private enqueueStart; /** Admit waiting starts while a slot is free or can be freed. */ pump(): Promise; /** * Longest-idle agent that may be stopped for a slot. Never one that is * busy, waiting for a start itself, always-on, holding an owner message, * Drive-warm, idle for less than the pressure idle timeout, or protected * by {@link ResourceModeDeps.protectedReason} (queued messages, pending * work, an open ticket, an open delegation). An agent that owes the owner * is never picked unless memory is critical. * * Anti-thrash rules (2026-10-16, Luna and Max evicting each other), which * hold in every mode, relaxed included, unless memory is critical: * - an agent idle for less than MIN_IDLE_BEFORE_EVICT_MS is kept (also for an owner-message waiter), * - an agent started less than MIN_RUN_BEFORE_EVICT_MS ago is kept, * - an agent whose work the waiter waits on ({@link ResourceModeDeps.waitsOn}) is kept, * - an agent that displaced the waiter less than DISPLACED_GUARD_MS ago is kept (hysteresis). * An owner-message waiter skips the first and last rule (owner messages are * rare, so they cannot thrash, and the owner must never wait); it keeps only * the waits-on rule and the protections above. * Relaxing only lifts the idle timeout; protected agents are never stopped. * When no agent qualifies, the waiter waits. * * @param running - Running agents * @param idleTimeoutMinutes - Pressure idle timeout; a younger idle is never stopped * @param relax - The first waiter has waited past RELAX_AFTER_MS: when no agent passes the rules, stop the best one anyway * @param ownerWaiter - The first waiter carries an owner message: the idle timeout does not protect an agent (the longest-idle one goes at once); busy, owner-message, Drive-warm and pending-work/ticket/delegation protections still apply * @param waiterName - The first waiter (for the waits-on and hysteresis rules) * @param now - Epoch ms of this pump * @returns The victim (if any), why each skipped agent was kept, and, when the rules were relaxed, why the victim had been kept */ private pickVictim; /** * Why stopping this agent for the waiter would thrash, or null when it would not. * * @param candidate - Running agent considered as a victim * @param waiter - First waiting start * @param now - Epoch ms of this pump * @param ownerWaiter - The waiter carries an owner message: the start window and the hysteresis do not apply (waits-on still does) * @returns English reason, or null */ private thrashReason; /** Log why a waiting start got no slot (at most once a minute per agent). */ private logNoSlot; private release; private schedulePump; } //# sourceMappingURL=resource-mode.service.d.ts.map