import type { WebDaemonInfo } from './web-protocol.js'; export interface StopDaemonResult { /** Daemons confirmed GONE at return — not merely asked to stop. */ readonly stopped: number; /** Every pid we asked to stop (gracefully or by signal). */ readonly pids: readonly number[]; /** Alive but not answering `/health`: recorded, deliberately not signalled. */ readonly orphanedPids: readonly number[]; } /** * Absolute path of the daemon entry. The NAME is fixed by the tree we are * running from, so the two supported modes each get a target that exists: * `dist/services/web/daemon-entry.js` after `pnpm build`, and * `src/services/web/daemon-entry.ts` for `pnpm dev` / the test suite. */ export declare function daemonEntryPath(): string; /** * Absolute path of the peaks CLI entry — the file that owns * `sub-agent shutdown register`. * * Explicit, never `process.argv[1]`: the caller of `registerWithParent` is the * DAEMON, whose `argv[1]` is the daemon entry, so spawning it with CLI * arguments boots a second daemon instead of registering anything (R8). */ export declare function cliEntryPath(): string; /** * Arguments that make `node` run `entry`. * * A `.ts` entry needs the TypeScript loader, and it is attached with * `--require preflight.cjs --import loader.mjs` — the exact flags `tsx` passes * to its own child — rather than by running the `tsx` CLI. * * That distinction is not cosmetic. `windowsHide` applies to the process we * spawn and to nothing it spawns in turn: launching `node tsx-cli.mjs entry.ts` * makes tsx spawn a GRANDCHILD, and the grandchild has no `windowsHide` of ours * — so on Windows every dev-mode `peaks web` cold start popped a console window * the user could not close. Invoking the loader directly keeps it one process, * so the flag we already pass covers everything. * * Exported so the race test's caller process is launched through the same * shape as the daemon: two copies of these flags would drift. * * Both callers prepend `process.execPath` themselves, so this returns ONLY the * interpreter flags and the entry. */ export declare function interpreterArgs(entry: string): string[]; /** * Whether this Node spells the ESM loader flag `--import`. * * It arrived in 20.6.0 (backported to 18.19.0); below that the spelling is * `--loader`, and passing `--import` is a bad option that kills the daemon at * startup — inside a `READY_TIMEOUT_MS = 20 s` wait whose only diagnosis is a * line in `daemon.log`. `engines.node` names a higher floor, but this function answers for * whatever interpreter the daemon is actually started with — a machine with several Nodes * can hand `--import` to an older one — so the flag cannot be unconditional. `tsx` gates * its own child the same way and this mirrors it. */ export declare function supportsImportFlag(nodeVersion?: string): boolean; /** * The exact command `spawnDaemon` hands to `spawn`, resolved without launching * anything so a test can assert its shape. * * `process.execPath` directly — never `npx`, and therefore never the * `cmd.exe /d /s /c node …` that npm's own `run-script` puts between us and the * daemon (`@npmcli/run-script/lib/make-spawn-args.js` spawns with `shell: true` * and no `windowsHide`). That layer was three processes per cold start instead * of one, it re-parsed our `--require` / `--import` paths through a command * string, it cost 2.8–7.1 s of the ~3 s cold start, and the two intermediate * processes ignored `windowsHide` — the console-window incident, one layer down. */ export declare function daemonSpawnCommand(): { command: string; args: string[]; }; /** * Spawn the detached daemon. Both stdio pipes are redirected to * `web/daemon/daemon.log` — a daemon that inherited our stdout, or wrote to * `cwd`, would be the sneakiest way to break AC1 (tech-doc §7.2 rule 6). */ export declare function spawnDaemon(projectRoot: string, sessionId: string): { pid: number | undefined; }; /** * Return a healthy daemon for this `(projectRoot, sessionId)`, spawning one only * when the session has none. A record whose pid is GONE is replaced (a stale * file from a SIGKILLed parent, R3); a record whose pid is ALIVE is never * replaced, even when `/health` does not answer — see `DaemonProbe`. * * Cold start is **double-checked locking**: the record, its pid liveness and * `/health` are re-probed AFTER the lock is acquired, with the same oracle the * pre-lock check used, and the lock is held across the spawn AND the readiness * wait. Q10 lets the orchestrator and a sub-agent call * `peaks web` at the same moment, and `spawnDaemon` returns as soon as `spawn()` * does, so releasing the lock there left the whole ~20 s startup window * unprotected: both callers would see no daemon, take the lock in turn, and * launch a browser each — two processes per session in violation of Q8, one of * them unreachable by `peaks web stop`. With the lock held, the loser waits for * the winner's daemon instead of spawning its own. */ export declare function ensureDaemon(projectRoot: string, sessionId: string): Promise; /** * Stop this session's daemon and clear its records. Scoped to * `(projectRoot, sessionId)` by construction — another worktree's daemon is * untouched (design §10.2). * * Ownership is proven before asking the process to stop (R6/R7): a planted * `daemon.json` naming an unrelated pid must not make `peaks web stop` kill a * process the caller does not own, and on Windows `SIGTERM` is * `TerminateProcess` with no chance to say "no". `/health` is NOT evidence — * it is answered before the auth check, so any unrelated local listener that * returns 2xx for a `GET` satisfies it. The proof is an AUTHENTICATED `/op` * whose reply reports the very identity the record claims (`isOwnDaemon`): a * recycled pid pointing at somebody else's dev server cannot produce it. * * The graceful path comes FIRST, and it is not a nicety: on Windows a signal * cannot be handled, so signalling would terminate the daemon without running * its teardown, leaving its chromium process behind — the exact false pass * AC6 is written against. `/op {op:'stop'}` lets the daemon close every context, * persist storage state, close the browser and exit on its own terms; the * signal is only the fallback for a daemon that accepts the request and then * fails to leave. * * An instance that is alive but cannot be proven ours is **not** signalled and * **not** de-recorded. Its record is the only handle on a running process: * clearing it would leave the daemon and its chromium detached with no verb * able to reach them, and would let the next cold start spawn a second daemon * beside a live one (Q8). It is reported in `orphanedPids`, and a daemon that * survives its own stop request stays recorded too, so `status` and a second * `stop` still see it. */ export declare function stopDaemon(projectRoot: string, sessionId: string): Promise; /** * Best-effort registration with the existing sub-agent shutdown registry * (Q7 / tech-doc §7.3). * * The entry point is `cliEntryPath()`, not `process.argv[1]`: this is called by * the daemon, whose `argv[1]` is the daemon entry, so using it spawned a second * daemon-entry with CLI arguments instead of registering anything (R8). * * Registration failing is non-fatal — the caller keeps running — but it must * not be silent: the daemon's stderr is `web/daemon/daemon.log` * (see `spawnDaemon`), so a spawn failure is written there. A detached child * whose `error` event has no listener would otherwise crash the daemon. */ export declare function registerWithParent(daemonPid: number, dispatchId: string): void;