/** * Discovery and single-instance coordination for the workflow daemon. * * Layout under the workflow home (`~/.agentprism/workflows` or AGENTPRISM_PERSISTENCE_ROOT): * * daemons/.json the FAMILY POINTER — "the current daemon for this env" * daemons/.lock the family's spawn lock (wx + stale-pid recovery) * daemons/instances/.json one record per live daemon, for `daemon status`/`stop --all` * * These files are hints, not truth: the CLI reconciles them against the OS process table * (`listDaemonProcesses`), so a daemon that lost or never wrote its record is still listed and * stoppable. Supersession is a one-way door: once the pointer names another daemon (or is gone), * the daemon it left behind stays superseded for life (the latch lives in createDaemon). * * A daemon serves every client with the env it was started with (the ACP backend registry is * resolved once at construction), so clients are keyed by their env fingerprint: every distinct * env gets its own daemon family and no two families ever contend. Inside a family, version is * a TOTAL ORDER: a shim supersedes a daemon that is strictly OLDER than itself and adopts one * that is equal or newer. That asymmetry is what makes succession converge — an old client * migrating off a superseded daemon can never resurrect its old code and flip discovery back. * * The pointer is written 0600 via tmp+rename after the port is bound and cleared (pid-guarded) * on graceful shutdown; readers always pid-check because a crash leaves files behind. */ import { DAEMON_NAME } from "./constants.js"; export interface DaemonInfo { name: typeof DAEMON_NAME; version: string; pid: number; /** The actually bound port — may differ from the default on bind-conflict fallback. */ port: number; url: string; startedAt: string; envFingerprint: string; /** Opaque process-generation identity. Absent on daemons predating run control. */ instanceId?: string; /** Signed loopback run-control endpoint. Absent on daemons predating run control. */ controlUrl?: string; /** Internal run-control protocol version. */ controlProtocol?: 1; } export interface SpawnLock { pid: number; startedAt: string; token: string; } /** The /healthz response body — the daemon's live identity and load, beyond daemon.json. */ export interface DaemonHealth { name: string; version: string; pid: number; port: number; startedAt: string; sessions: number; activeRuns: number; envFingerprint: string; projects: Array<{ projectDir: string; activeRuns: number; }>; instanceId?: string; controlProtocol?: 1; /** * True when this daemon has been superseded — a newer daemon owns its family's discovery * pointer (it names a different pid) so this one is a lame duck: it admits no new sessions, * migrates its idle sessions to the successor, and exits as soon as nothing is in flight. * Absent on daemons predating the succession model; readers treat a missing field as * "not a lame duck". */ lameDuck?: boolean; /** Requests currently being processed (in-flight POSTs) across all sessions. */ inflightRequests?: number; } /** * Probe a daemon's /healthz. Returns undefined on any failure — callers treat that as "no * live daemon at this port" (a foreign process answering is filtered by the name check). */ export declare function probeHealthz(port: number, timeoutMs?: number): Promise; /** The family pointer for `fingerprint` (default: this process's env). */ export declare function daemonInfoPath(fingerprint?: string): string; /** The family spawn lock for `fingerprint` (default: this process's env). */ export declare function daemonLockPath(fingerprint?: string): string; /** One record per live daemon process, whatever its family. */ export declare function daemonInstancePath(pid: number): string; export declare function daemonLogPath(): string; export declare function pidIsAlive(pid: number): boolean; /** The family pointer's record (default: this process's env family). */ export declare function readDaemonInfo(fingerprint?: string): DaemonInfo | undefined; /** One PID-guarded daemon generation record, if readable. */ export declare function readDaemonInstance(pid: number): DaemonInfo | undefined; /** * Atomically (tmp+rename) repoint the family pointer of `info.envFingerprint` at `info`, and * record the instance. For a succession this is the step that demotes the predecessor to a * lame duck (its pid no longer matches the pointer). */ export declare function writeDaemonInfo(info: DaemonInfo): void; /** * Remove the instance record for `pid`, and the family pointer only when it still names `pid` * — never clobber a successor's pointer. The pointer checked is the one the instance record * names (falling back to this process's env family). */ export declare function clearDaemonInfo(pid: number): void; /** * True when this daemon no longer owns its family's discovery: the pointer names a pid other * than `ownPid`, or is gone. Such a daemon is a "lame duck" — it keeps serving in-flight work, * admits no new sessions, migrates its idle sessions to the successor, and exits as soon as * nothing is in flight. * * A missing pointer counts: a daemon that published its pointer and later finds it gone has * been superseded and its successor has since exited (a successor clears only a pointer naming * itself). createDaemon applies this only once the pointer has been seen naming the daemon, and * latches the first true — nothing ever repoints discovery at an older daemon, and treating a * vanished pointer as "back in service" is how zombie daemons used to accumulate. */ export declare function isSupersededBy(ownPid: number, fingerprint?: string): boolean; export interface DaemonInstance { info: DaemonInfo; } /** * Every daemon with a live instance record (dead ones are pruned as a side effect). Liveness is * a pid check only; callers probe /healthz for identity and load, and reconcile against * `listDaemonProcesses` for daemons that have no record. */ export declare function listDaemonInstances(): DaemonInstance[]; /** A live daemon of ours (any family) bound to `port`, if any. */ export declare function findDaemonInstanceOnPort(port: number): DaemonInstance | undefined; export interface DaemonProcess { pid: number; /** The process's command line as the OS reports it. */ args: string; } /** * Every live daemon process of ours the OS knows about, whatever it recorded (or failed to * record) on disk — the ground truth `daemon status` and `daemon stop --all` reconcile the * instance records against, so a daemon can never hide from the CLI. Only this user's * processes; never this process itself. POSIX only (`ps`); on Windows the records are all * there is. */ export declare function listDaemonProcesses(): DaemonProcess[]; /** * True when `pid` is alive AND is a daemon process of ours — the identity check that keeps a * stale record whose pid the OS has since reused from ever being signalled. On POSIX a `ps` * that fails or knows no such process means "not verified", never "assume it is ours"; on * Windows (no process table here) this degrades to the pid liveness check. */ export declare function isDaemonProcess(pid: number): boolean; export declare function envFingerprint(env?: Record): string; /** * Semver-style ordering (`major.minor.patch[-prerelease]`): negative when `a` is older than * `b`, zero when equal, positive when newer. A prerelease sorts below its release; anything * unparseable falls back to a plain string comparison so the order stays total. */ export declare function compareVersions(a: string, b: string): number; /** * Claim the right to spawn the family's daemon. Returns null when another live process holds * the claim (the caller should poll the pointer/health instead of spawning). A lock owned by a * dead pid is recovered once. */ export declare function claimSpawnLock(fingerprint?: string): SpawnLock | null; export declare function releaseSpawnLock(lock: SpawnLock, fingerprint?: string): void; //# sourceMappingURL=daemon-info.d.ts.map