/** * Ownership claims for managed dev servers. * * WHY THIS EXISTS * `servers.json` records no owner, and every session in a directory reattaches * to the same running servers, so no session could tell its own dev server * from someone else's. That left two failures with one cause (issue #139): * a window closed for good left its dev servers running until reboot, and an * idle suspend could not release dev servers at all, because stopping one * might pull it out from under another window still using it. * * WHAT A CLAIM IS * One file per claim, named `--.json`. A claim is * valid exactly while its `supervisorPid` names a live process. The supervisor * is the right anchor because it is the only process whose lifetime IS the * session's: it survives the child dying on suspend and on every rebuild * restart, and it dies when the client does (see supervisor/client-watcher.ts). * A claim held by the MCP child instead would evaporate during a two-second * rebuild, and another session suspending in that window would stop a server * that is very much in use. * * WHY A DIRECTORY RATHER THAN ONE FILE * Each file has exactly one writer, ever - the session named in its own * filename. Creating is an atomic temp+rename, releasing is an unlink, and * nothing anywhere does read-modify-write, so concurrent sessions cannot race. * (`servers.json` itself is not safe this way: ServerManager's save mutex is * per-process, so two sessions writing it can still lose an update. Putting * owner fields in there would inherit that.) * * PRESENCE, NOT JUST CLAIMS * A claim is taken when a session starts a server or reattaches to one at * startup. That alone is not enough: a window already open when another window * starts a dev server never gets to claim it, and would then have it stopped * out from under it - the common ordering, not an edge case. So each session * also records its presence in a project directory, and a server is protected * while any other live session is working where it runs. That makes the rule * independent of who started what, and of the order things happened in. * * THE RULE THIS SUPPORTS * Stop a server only when no other LIVE session either claims it or is working * in its directory. Anything unknown - unreadable directory, unparseable * record, a liveness check that throws - counts as "someone might need it", so * the server is left alone. The costs are asymmetric: under-stopping leaks a * process until the next garbage collection, over-stopping destroys work that * was running. */ export interface ServerClaim { serverId: string; /** The claiming session's supervisor. Liveness of this pid IS the claim's validity. */ supervisorPid: number; /** Start time of that supervisor, to survive pid reuse. Empty when unavailable. */ supervisorStartedAt: string; /** Diagnostics only - which child process wrote the claim, and when. */ childPid: number; cwd: string; createdAt: string; } /** * When a pid started, as the OS reports it. * * A pid alone cannot survive reuse: a recycled pid makes a dead session look * alive, which pins its servers forever. Comparing start times distinguishes * "still the same process" from "some new process wearing its number". * Returns '' when it cannot be read, and callers treat that as "cannot * disprove" rather than as a mismatch. */ export declare function readProcessStartTime(pid: number): string; export interface SessionPresence { supervisorPid: number; supervisorStartedAt: string; childPid: number; cwd: string; startedAt: string; } export interface ClaimsStoreOptions { /** Own supervisor pid; overridable for tests. */ supervisorPid?: number; /** Reads a pid's start time; overridable for tests. */ startTimeReader?: (pid: number) => string; /** Liveness check; overridable for tests. */ isAlive?: (pid: number) => boolean; } /** * Reads and writes the claim files for one storage scope (local or global), * mirroring where `servers.json` itself lives so a claim always sits beside * the record it refers to. */ export declare class ServerClaimsStore { private readonly supervisorPid; private readonly readStartTime; private readonly isAlive; private readonly ownStartTime; constructor(options?: ClaimsStoreOptions); getOwnSupervisorPid(): number; private dir; private claimPath; /** * Every claim on disk for a scope, dead ones included. Anything unreadable or * unparseable is skipped rather than thrown: a corrupt claim must not be able * to break a shutdown path. */ readAll(global: boolean): Array<{ path: string; claim: ServerClaim; }>; /** * Whether a claim is still valid: its supervisor is alive, and (when both * start times are readable) it is the same process that made the claim * rather than a recycled pid. */ isClaimLive(claim: ServerClaim): boolean; /** Record this session as an owner of `serverId`. Idempotent. */ claim(serverId: string, cwd: string, global: boolean): Promise; /** Drop this session's claim on `serverId`, if it holds one. */ release(serverId: string, global: boolean): void; /** Drop every claim this session holds, across both scopes. */ releaseAllOwn(): void; /** * Whether a live session OTHER than this one claims `serverId`. */ hasForeignLiveClaim(serverId: string, global: boolean): boolean; private sessionsDir; private sessionPath; /** Announce that this session is working in `cwd`. */ registerSession(cwd: string): Promise; /** Withdraw this session's presence record. */ unregisterSession(): void; /** Every presence record on disk, dead ones included. */ readSessions(): Array<{ path: string; presence: SessionPresence; }>; /** * Whether another live session is working in `cwd`. * * This is what makes the rule independent of ordering: a window that was * already open when someone else started a dev server never had a chance to * claim it, but it is plainly still using the project. */ hasOtherLiveSessionIn(cwd: string): boolean; private isPresenceLive; /** Delete presence records whose session is gone. */ collectDeadSessions(): number; /** * The whole decision, in one place: may this session stop `serverId`, * running out of `serverCwd`? */ mayStop(serverId: string, serverCwd: string, global: boolean): boolean; /** * Delete claims whose owning session is gone, and report which servers were * left with no live claim at all - those are the ones nobody is coming back * for. */ collectDeadClaims(global: boolean): { removed: number; unclaimedServerIds: string[]; }; } /** The store used by the running server. */ export declare const serverClaims: ServerClaimsStore; //# sourceMappingURL=server-claims.d.ts.map