/** Single source of truth for the slice unit name. Used by the spawn argv, * the boot-seed list-units filter, and the installer's slice unit * emission. */ export declare const CLAUDE_PTYS_SLICE = "claude-ptys.slice"; /** Hard cap on SIGTERM→SIGKILL escalation inside the scope. systemd * delivers SIGTERM on stop, waits TimeoutStopSec, then SIGKILL. 5 s * matches the manager's prior killGraceMs (`config.killGraceMs`); the * manager no longer manages the escalation itself. */ export declare const SCOPE_TIMEOUT_STOP_SEC = 5; /** Result of one systemctl invocation. Mirrors the shape of * spawnSync('utf8') return — exit code plus captured streams. The * scope-stop and list-units paths both go through this shape so tests * can inject a single stub. */ export interface SystemctlResult { exitCode: number; stdout: string; stderr: string; /** Task 1398 gap 2 — true when the `systemctl` binary itself is missing * (ENOENT), i.e. this host has no systemd (macOS). Distinct from a non-zero * exit, which means systemctl ran and refused. `stopScopeUnit` uses this to * fall back to a direct pid kill. Injected test runners never set it, so the * linux/systemctl test path is unaffected. */ absent?: boolean; } /** Synchronous runner for `systemctl `. Default impl uses * spawnSync (matches uninstall.ts's existing idiom and keeps the call * site free of async glue for what is in practice a sub-50ms call). * Tests inject a recording stub. */ export type SystemctlRunner = (args: readonly string[]) => SystemctlResult; export declare function defaultSystemctlRunner(args: readonly string[]): SystemctlResult; export declare function scopeUnitName(unitToken: string): string; /** Inverse of `scopeUnitName` — extracts the unitToken from a unit name * emitted by `systemctl --user list-units --type=scope 'claude-session-*.scope'`. * Returns null when the name doesn't match the prefix/suffix shape. */ export declare function unitTokenFromScopeUnit(unit: string): string | null; /** One row from `systemctl --user list-units --type=scope --no-legend * 'claude-session-*.scope'`. The `loaded`/`active`/`sub` columns let the * boot-seed reconciler ignore units that are draining (`sub=stop-sigterm`) * vs running (`sub=running`). */ export interface SurvivingScope { unitName: string; unitToken: string; loaded: string; active: string; sub: string; } /** Parse the `--no-legend` output of `systemctl --user list-units`. * Tolerant of leading column whitespace and trailing description column. * Rows whose unit name doesn't extract a unitToken are skipped (defensive * against future systemctl-output drift). */ export declare function parseSurvivingScopes(stdout: string): SurvivingScope[]; /** Issue `systemctl --user list-units --type=scope --no-legend * 'claude-session-*.scope'` and return the parsed rows. The boot-seed * uses this to discover surviving PTYs across a manager restart. */ export declare function listSurvivingScopes(run: SystemctlRunner): { scopes: SurvivingScope[]; result: SystemctlResult; }; /** Stop any orphan rc scope units left over from prior session-manager * boots. Called from the session-manager startup BEFORE the rc-daemon * supervisor's first spawn and BEFORE the HTTP server begins accepting * /rc-spawn requests, so no in-flight scope can be misidentified as an * orphan. Idempotent and safe to call on every boot — if nothing * matches, no stop is issued. */ export declare function reapOrphanNonPtyScopes(run: SystemctlRunner, logger: (line: string) => void): Promise; /** Outcome surface for a scope-stop attempt. * * Task 451 — `outcome=ok` means **unit stopped AND pid is gone**. Pre-451, * `ok` was emitted whenever `systemctl stop` exited 0, even when the unit * was already collapsed and the underlying claude pid had been reparented * outside the scope cgroup (still running). The added post-flight * `kill(pid, 0)` check distinguishes that case: * * - `ok` — stop returned 0 AND (pid was not supplied OR pid is gone) * - `already-gone` — stop returned exit code 5 (unit not loaded) * - `escalated-term` — pid survived `systemctl stop`; SIGTERM cleared it within grace * - `escalated-kill` — SIGTERM didn't suffice; SIGKILL cleared it * - `failed-pid-survives` — pid is still alive after SIGKILL (rare; signal-blocked * process, or a zombie whose parent has not reaped). Caller surfaces as a * distinct error class. * - `error` — systemctl exit was non-zero and non-5 (a real systemctl * failure unrelated to unit state) */ export type StopScopeOutcome = 'ok' | 'already-gone' | 'escalated-term' | 'escalated-kill' | 'failed-pid-survives' | 'error'; export interface StopScopeResult { outcome: StopScopeOutcome; exitCode: number; stderr: string; } /** Injectable killer + alive-check for tests; production wires * `process.kill`. `kill(pid, 0)` returns whether the pid is currently * reachable (alive AND signal-permitted by the caller's uid). */ export interface ProcessSignals { /** Send `signal` to `pid`. Throws on ESRCH (no such pid) or EPERM. */ kill(pid: number, signal: number | string): void; /** True when `pid` is alive and the caller has permission to signal it. * ESRCH → false. EPERM → true (alive but not ours; treat as alive so * we never mistakenly call the pid dead). Any other throw → true, * on the same "fail safe, escalate" principle. */ isAlive(pid: number): boolean; } export declare const defaultProcessSignals: ProcessSignals; /** Issue `systemctl --user stop --no-block ` and verify the kill. * * Task 451 — when `pid` is supplied, after `systemctl stop` returns we * check `kill(pid, 0)`. If the pid is still alive (the scope was already * collapsed before stop ran, so the pid was reparented outside the cgroup * and stop's `exit=0` was a lie), escalate: SIGTERM → wait `killGraceMs` * → re-check → SIGKILL → wait → re-check. Outcome reflects the path * taken. When `pid` is null (caller cannot supply, e.g. genuine orphan * with no MainPID), the function behaves as pre-451 — exit code is the * only signal. * * The --no-block flag returns once the stop request is queued; the * scope's TimeoutStopSec=5 handles in-cgroup SIGTERM→SIGKILL escalation. * Exit code 5 ("not loaded") still means the scope retired naturally — * treated as `already-gone`. */ export declare function stopScopeUnit(run: SystemctlRunner, unitToken: string, opts?: { pid?: number | null; killGraceMs?: number; signals?: ProcessSignals; }): Promise; /** Argv shape produced by `buildSystemdRunArgv`. The cmd is always * `systemd-run`; the manager passes this to node-pty. */ export interface SystemdRunArgv { cmd: 'systemd-run'; args: string[]; } export interface BuildSystemdRunArgvParams { unitToken: string; claudeBin: string; claudeArgs: readonly string[]; /** Full env map to thread into the scope via `--setenv=KEY=VAL`. Every * pair lands as one flag. The default systemd-run inherit-from-caller * is NOT relied on; spawnPtyAdapter composes the explicit env block * (per-spawn overrides + a copy of process.env it wants in the scope) * and the helper threads exactly that shape. */ env: Readonly>; /** Working directory passed via `--working-directory=`. The manager's * prior node-pty spawn passed `cwd`; the scope receives it the same way. */ cwd: string; /** Task 1222 — optional hard per-session memory ceiling (bytes). When * set, the scope is created with `-p MemoryMax=` so the kernel * cgroup-OOMs the largest process inside THIS session's scope when it * balloons, instead of letting it thrash the host. Omitted for the * rc-daemon scope (the spawner is never capped). */ memoryMaxBytes?: number; } /** Build the systemd-run argv that node-pty wraps for one claude spawn. * Inside the scope, `/bin/sh -c 'trap "" HUP; exec "$@"' sh ` * wraps the launch so: * * - **claude ignores SIGHUP** when the manager closes the PTY master fd * on restart. Without the trap, master-close → kernel SIGHUP → * claude exits. * - **`exec` keeps the pid stable** through the chain (sh → claude * reuse the original pid via execve), preserving the * `pty.pid == claude.pid` invariant the watcher.waitForPid path * depends on. * * Task 573 — node-pty allocates the TTY for the child. The earlier * script(1) wrap (Task 556) plus the non-PTY scope primitive it served * are gone. */ export declare function buildSystemdRunArgv(p: BuildSystemdRunArgvParams): SystemdRunArgv; /** Query `systemctl --user show -p MainPID claude-session-.scope` * and return the main pid (the claude process). Boot-seed uses this to * bridge from the surviving scope unit to the PID file claude wrote at * `$CLAUDE_CONFIG_DIR/sessions/.json`, which in turn carries claude's * intrinsic sessionId. Returns null when the unit has no main pid (the * scope is draining) or when the systemctl call fails. */ export declare function getScopeMainPid(run: SystemctlRunner, unitToken: string): number | null; /** Task 1398 gap 2 — platform-branching spawn plan. macOS has no `systemd-run`; * wrapping a spawn in it makes node-pty exec a missing binary, which the child * fails as `exitCode=1 ranMs=5 bytes=0`. On darwin, spawn claude directly and * pass the built child env as the pty env (no `--setenv` indirection). On every * other platform this is exactly today's systemd-run wrapper, with the manager's * process.env as the pty env. Pure. */ export declare function resolveSpawnPlan(platform: NodeJS.Platform, p: BuildSystemdRunArgvParams): { cmd: string; args: string[]; ptyEnvSource: 'child' | 'process'; }; /** Task 1398 gap 2 — darwin session stop. macOS has no `systemctl`, so * `stopScopeUnit` returns `error` before any kill. This stops a session by a * direct SIGTERM -> grace -> SIGKILL -> grace escalation on the pty pid, * returning the same outcome union so the stopSession call site is unchanged. * Mirrors stopScopeUnit's post-451 escalation exactly, minus the systemctl * stop. `exitCode: 0` throughout (no systemctl exit to report). */ export declare function stopPidDirect(pid: number | null, opts?: { killGraceMs?: number; signals?: ProcessSignals; sleepFn?: (ms: number) => Promise; }): Promise; /** Task 1399 — capacity-eviction stop. The eviction path (pty-spawner * checkSlotsAndMaybeEvict) drops an idle specialist to make room; on Linux it * fires `systemctl --user stop --no-block ` and lets the scope's * TimeoutStopSec own the in-cgroup SIGTERM->SIGKILL escalation, so the pid is * intentionally omitted (no post-flight verify — the victim is still active). * * On darwin there is no scope: the session was bare-spawned (resolveSpawnPlan), * so `systemctl` is absent and a pid-less `stopScopeUnit` would fall back to * `stopPidDirect(null)` and return `already-gone` without killing anything — * the evicted pty would linger until the manager exits. This helper branches * on platform: darwin kills `victim.pid` directly; every other platform issues * exactly today's pid-omitted scope stop, byte-identical. */ export declare function evictSessionProcess(platform: NodeJS.Platform, run: SystemctlRunner, victim: { scopeUnitToken: string; pid: number; }, opts?: { killGraceMs?: number; signals?: ProcessSignals; sleepFn?: (ms: number) => Promise; }): Promise; //# sourceMappingURL=systemd-scope.d.ts.map