/** The unified (cgroup v2) line of /proc//cgroup is `0::`; v1 * controller lines (`::`) are ignored. Returns null * when the blob carries no v2 line. */ export declare function parseCgroupV2Path(blob: string): string | null; /** The shared slice a session scope hangs under. Returns the parent path only * when that parent is CLAUDE_PTYS_SLICE — a scope somewhere else is not a * session cohort and must not be reported as one. */ export declare function sliceCgroupPathFromScope(scopeCgroup: string): string | null; /** Pure core of the per-session read: the session's OWN scope cgroup from a * /proc//cgroup blob, or null when the blob resolves no v2 path (a * cgroup v1 host) or the leaf is not this session's scope. * * Path segments cannot contain '/', so requiring the leading slash makes this * an exact final-segment match — `claude-session-a.scope` cannot satisfy a * request for `claude-session-aa.scope`, and a pid that escaped to the parent * slice (Task 451: a collapsed scope reparents its pid outside the cgroup * while the process keeps running) does not match either. */ export declare function validatedScopeCgroup(blob: string, expectedUnit: string): string | null; /** memory.current is bytes; every snapshot field is kB. One rounding, here. */ export declare const bytesToKb: (bytes: number) => number; export type Logger = (line: string) => void; /** Task 1749 — one live session's process-side facts. Replaces the anonymous * `memListKb: number[]` the census carried through Task 1707: the install-wide * activity view renders one row per session, so the RAM figure has to arrive * attached to the session it belongs to. `formatPtyCensus` derives its * memSum/memMax from this array, so the census log line is unchanged. */ export interface CensusSession { /** The tracker's intrinsic Claude session id — the key every other activity * source (watcher rows, jsonl enumerator, metering) is also keyed by. */ sessionId: string; /** This session's scope cgroup `memory.current`, converted to kB. The WHOLE * subtree — the PTY leader plus every MCP child the kernel charges to that * scope — not the leader alone. */ memKb: number; /** Δusage_usec / Δwall_usec × 100 against the previous tick's sample for * this same scope. Can exceed 100: a session is a leader plus ~23 MCP * children, so two fully-busy cores read 200. * * NULL, never 0, in four cases: this scope's first sample, a prior sample * whose cgroup path differs (the scope was recreated, and a delta against a * foreign scope is not this session's CPU), an unreadable `cpu.stat`, and * two collects inside the same millisecond. 0 would be a false statement * that the session is idle; null says the rate is unmeasured. */ cpuPct: number | null; } /** One retained CPU reading for a scope, carried between ticks. The cgroup path * travels with the counter because `usage_usec` resets when a scope is * recreated — comparing across that boundary would report a wild negative or * a meaningless spike. */ export interface CpuSample { cgroup: string; usageUsec: number; atMs: number; } /** Prior CPU samples, keyed by sessionId. */ export type CpuSampleMap = Map; export interface PtyCensusSnapshot { /** Count of live trackers (livePtyCount()). */ live: number; /** RC_DAEMON_CAPACITY — the daemon's own concurrent-session cap. */ capacity: number; /** One entry per live session whose scope resolved. Sessions whose scope * could not be read are excluded here and counted in goneCount or * unattributedCount. */ sessions: CensusSession[]; /** Task 1749 — wall clock at collect time, from the injected `now()`. The * activity view renders `now - sampleAtMs` so a snapshot from a dead * collector reads as stale rather than as current. */ sampleAtMs: number; /** Live sessions that are GONE — the pid vanished, or its scope cgroup was * torn down mid-tick. Benign and transient: the RAM went with them. * Counted, never silently dropped. */ goneCount: number; /** Live sessions that are ALIVE but whose subtree charge could not be * attributed: the pid resolved a cgroup that is not its own scope (Task * 451 — a collapsed scope reparents the pid outside its cgroup while the * session keeps running), the host is cgroup v1, or the tracker's scope * token is malformed. Distinct from goneCount because the RAM is still * held: folding these into "gone" would hide a live ~1.3GB session from * the gauge that exists to bound it — the same under-reporting this task * corrects. A non-zero value here means memSumMb is low by that many * sessions. */ unattributedCount: number; /** `claude-ptys.slice` `memory.current` (kB), or null when no live scope * resolved the slice path (no live session, or none attributable) or the * slice read failed. Null renders as `sliceMb=unknown`, never 0 — 0 would * be a false statement about a BOX-WIDE figure, since this brand having no * live session says nothing about what the shared slice holds. * BOX-WIDE and shared: every co-resident brand runs as the same uid and * lands in this one slice, so every brand's census prints the SAME figure. * It is not this brand's share. */ sliceKb: number | null; /** Task 1709 — `claude-ptys.slice` memory.stat `anon` (kB), or null on the * same terms as sliceKb. NOT rendered on the census line: this census * reports a ceiling and states it in memory.current, the units MemoryHigh * enforces. anon is carried for the consumer that needs the slice's * unreclaimable footprint — sizing a bound against a MemAvailable-derived * residual, where memory.current would double-count page cache that * MemAvailable already calls available. See slice-memory-policy.ts. */ sliceAnonKb: number | null; /** Task 1709 — `claude-ptys.slice` memory.high (kB): the bound in force, or * null when unset/unreadable. Also not rendered. Carried so a policy that * writes this bound can compare against reality rather than its own memory * of what it wrote — four co-resident brands write the one file. */ sliceHighKb: number | null; /** /proc/meminfo MemTotal (kB). */ sysTotalKb: number; /** /proc/meminfo MemAvailable (kB). Named "avail" (not "free") because the * source is MemAvailable and the rendered field is sysAvailMb. */ sysAvailKb: number; /** /proc/sys/fs/file-nr field 1 — allocated file handles. */ fdOpen: number; /** /proc/sys/fs/file-nr field 3 — max file handles. */ fdMax: number; } /** Pure renderer — no I/O. One structured line from an injected snapshot. */ export declare function formatPtyCensus(s: PtyCensusSnapshot): string; /** The `anon` line of a cgroup's memory.stat, in bytes. Null when absent or * malformed. Pure — the blob is read by the caller. * * Task 1709 — anon is the slice's UNRECLAIMABLE footprint, and it answers a * different question from memory.current. memory.current = anon + file + slab * + …, and `file` is page cache the kernel will hand back under pressure — * which /proc/meminfo's MemAvailable ALREADY counts as available. Any * arithmetic that subtracts memory.current from a MemAvailable-derived * residual therefore double-counts that cache. anon does not have that * overlap, which is why the slice policy sizes its bound from it. */ export declare function parseCgroupAnonBytes(blob: string): number | null; /** Task 1749 — the `usage_usec` line of a cgroup's cpu.stat, in microseconds. * Null when absent or malformed. Pure — the blob is read by the caller. * * This is the scope's CUMULATIVE CPU time across the whole subtree (leader * plus every MCP child), which is why a rate needs two samples. The anchored * match is load-bearing: `user_usec` and `system_usec` are siblings in the * same file and each is a fraction of the total, so a loose match would * silently understate every session's CPU. */ export declare function parseCgroupUsageUsec(blob: string): number | null; /** One session's read outcome. Discriminated rather than a bare null so the * collector can keep "the RAM went away" (gone) apart from "the RAM is still * held but I could not attribute it" (unattributed) — see the counters on * PtyCensusSnapshot. */ export type ScopeMemResult = { kind: 'ok'; memKb: number; cgroup: string; } | { kind: 'gone'; } | { kind: 'unattributed'; }; /** The census's whole I/O surface, injectable so the collector's gone-scope, * unattributed-scope and slice-derivation behaviour is unit-testable off-Pi. */ export interface CensusIo { /** The session's own scope charge (kB) and the validated cgroup path it was * read from. */ scopeMem(pid: number, expectedUnit: string): ScopeMemResult; /** claude-ptys.slice memory.current (kB) given any validated scope path. * Null when the parent is not the session slice or the read failed. */ sliceMem(scopeCgroup: string): number | null; /** Task 1709 — claude-ptys.slice memory.stat `anon` (kB). Null on the same * terms as sliceMem. Reported alongside memory.current, never instead of it: * they answer different questions and the census's own ceiling is stated in * memory.current because that is what MemoryHigh enforces. */ sliceAnon(scopeCgroup: string): number | null; /** Task 1709 — claude-ptys.slice memory.high (kB), i.e. the bound actually * in force. Null when unset ("max" — readCgroupNumber rejects non-numeric) * or unreadable. A policy that re-derives this bound must compare against * the live value, not against what it believes it last wrote: four * co-resident brands write this one file. */ sliceHigh(scopeCgroup: string): number | null; /** Task 1749 — the scope's cumulative CPU microseconds (cpu.stat * `usage_usec`). Null when the file is absent or malformed: a scope torn * down mid-tick, a host without the cpu controller enabled, or a non-Linux * host. Read only for a scope that already resolved `ok`, so a gone session * costs no extra syscall. */ scopeCpuUsec(scopeCgroup: string): number | null; /** Wall clock. Injected so the CPU delta arithmetic is testable without * sleeping, and so `sampleAtMs` is deterministic under test. */ now(): number; meminfoKb(): { sysTotalKb: number; sysAvailKb: number; }; fileNr(): { fdOpen: number; fdMax: number; }; } export declare const defaultCensusIo: CensusIo; /** Impure — gathers a snapshot from the live tracker map, /proc and the scope * cgroups. Two file reads per live session (/proc//cgroup to find the * scope, then that scope's memory.current) plus one slice read; no process * spawns. A session whose scope cannot be read contributes nothing to * memListKb and increments goneCount (never silently dropped). On a non-Linux * host there is no /proc and no scope (sessions are bare-spawned), so every * live session counts as gone and the memory/fd fields read 0 — as before. */ export declare function collectPtyCensus(io?: CensusIo, prior?: CpuSampleMap): { snapshot: PtyCensusSnapshot; samples: CpuSampleMap; }; export interface PtyCensus { start(): void; stop(): void; /** Collect + log one census line synchronously. Returns the snapshot. */ tickOnce(): PtyCensusSnapshot; /** Task 1749 — the most recent snapshot, or null before the first tick. * `GET /census` serves this rather than collecting inline: an inline * collect has no prior sample, so every cpuPct it produced would be null. */ latest(): PtyCensusSnapshot | null; } export interface PtyCensusDeps { intervalMs: number; logger: Logger; /** Task 1749 — emit one `op=cpu-sample` line per scope per tick. * * Off by default: it is one line per live session per 60s, which is real * journal volume for a signal only needed while calibrating. * * It exists because the CPU sampling interval must NOT be picked by * assumption. `wallMs` is the actual elapsed time between this scope's two * samples, and if its jitter approaches the interval then the percentage is * noise and the interval has to rise. That decision needs the measurement, * so the line is the instrument for it. Enable via CENSUS_CPU_SAMPLE_LOG=1. */ logCpuSamples?: boolean; } export declare function createPtyCensus(deps: PtyCensusDeps): PtyCensus; //# sourceMappingURL=pty-census.d.ts.map