/** * Shared spawn infrastructure used by the per-agent spawn modules * (claude-spawn / copilot-spawn / codex-spawn). * * Holds the pieces that are agent-agnostic: env sanitization, CLI-path * resolution helpers, MCP transport parsing, process registration/exit * bookkeeping, and the startup-cleanup guard. Keeping these here lets each * agent module stay focused on its own CLI invocation without duplicating the * lifecycle plumbing. */ import { type ThreadLifecycleService } from "./thread-lifecycle.service.js"; import type { SensoriumTransport } from "./mcp-config.service.js"; /** Result shape shared by every spawn function. */ export type SpawnResult = { pid: number; logFile: string; } | { error: string; }; export declare const sanitizeSpawnEnv: (extra?: Record) => NodeJS.ProcessEnv; /** * Drop any operator-supplied custom env var whose key the denylist blocks. Custom * vars are applied as an explicit override (not from process.env) so they would * otherwise bypass sanitizeSpawnEnv's denylist — this keeps a custom entry from * smuggling a secret (e.g. CLAUDE_CODE_OAUTH_TOKEN) or a cross-agent BYOK key into * a child. The locked built-ins that legitimately carry those keys are injected * separately and are unaffected. */ export declare const stripDenylistedEnvVars: (vars: Record) => Record; /** Test hook: clear the CLI resolution cache. */ export declare function resetCliPathCacheForTest(): void; export declare const resolveCliPath: (name: string, prefer?: RegExp) => string | null; /** On Windows: prefer .exe, fall back to .cmd, refuse extensionless shims (e.g. Volta). */ export declare function resolveWindowsCliPath(name: string): string | null; export declare const normalizeWorkingDirectory: (workingDirectory?: string) => string | undefined; export declare const isStartupCleanupInProgress: () => boolean; export declare const setStartupCleanupInProgress: (v: boolean) => void; /** * Shared spawn preflight: refuse when the dashboard's max-concurrent-threads * limit is already reached, then require the MCP HTTP transport. Kept here so * both checks and their error wording apply uniformly to Claude, Copilot and * Codex spawns rather than drifting per agent. */ export declare function checkSpawnPreconditions(agentLabel: string): { transport: SensoriumTransport; } | { error: string; }; /** * Per-thread log file: sanitized name + agentType + threadId + date under * THREAD_LOGS_DIR. The agentType segment sits BEFORE threadId so each agent * type writes to its own single-format file even when a thread's type is * switched on the same day (otherwise claude stream-json and copilot plain-text * lines interleave in one file and break the dashboard log stream). Placing it * before threadId also preserves the `__.jsonl?$` suffix that * the mtime-based log consumers match on. */ export declare function buildThreadLogPath(name: string, agentType: string, threadId: number, ext: "json" | "jsonl"): string; /** Session-bootstrap prompt shared by the Claude and Copilot CLIs (Codex * delivers its own autonomous-loop prompt over stdin instead). */ export declare const buildSessionPrompt: (name: string, threadId: number) => string; /** * Everything needed to start an agent process, agent-specific policy already * applied (env assembly, CLI args, prompt delivery). The SAME plan drives both * spawn mechanics: POST /proc/spawn on the supervisor broker (the process * owner target state, plan §4.1) and the legacy local `spawn()` fallback. */ export interface AgentLaunchPlan { threadId: number; name: string; logFilePath: string; configPath: string; agentLabel: string; memorySourceThreadId?: number; threadType?: "worker" | "branch"; exe: string; args: string[]; env: NodeJS.ProcessEnv; cwd?: string; /** Prompt-sized payload written to the child's stdin then EOF'd (Codex); * undefined → stdin ignored/NUL. */ stdinData?: string; } export type BrokerSpawnMode = "required" | "off"; /** * Spawn-mechanism switch (plan §4.1 — the supervisor broker is the SOLE spawn * mechanism on Windows). Two states only: * SENSORIUM_BROKER_SPAWN=1 broker REQUIRED — the supervisor owns spawn; a * broker failure fails the spawn LOUDLY, never a * silent uncontained local fallback; * SENSORIUM_BROKER_SPAWN=0 forces local spawning on every platform incl. * Windows; used for dev / non-Windows. * unset win32 → "required", everything else → "off". * * Job objects are Windows-only, so non-Windows resolves to "off" unless forced. * * The one narrow exception to "required never local-spawns" lives in * launchAgent: a supervisor whose /proc/spawn endpoint is ABSENT (HTTP 404/401 * — an OLD binary that predates the endpoint) gets a self-healing ONE-RELEASE * grace: launchAgent WARNs (re-run Install-Sensorium to update the supervisor) * and local-spawns once, so an MCP update can never strand agents behind an old * supervisor binary. Those locally-spawned processes are adopted into * jobs+registry by the supervisor's boot reconciliation / /proc/adopt. A * supervisor that is merely DOWN (connection refused / timeout) does NOT get * this grace — its endpoint would exist if it were up, so a local spawn there * would just reopen the uncontained-zombie hole. */ export declare function brokerSpawnMode(): BrokerSpawnMode; export declare function spawnReconcileDelayForTest(ms: number | null): void; /** * Launch an agent process for a thread. On Windows the supervisor broker is the * SOLE spawn mechanism (mode "required"): the supervisor owns spawn/observe/kill * and children land in the thread's job object so the whole tree stays killable. * In dev / non-Windows (mode "off") the legacy local `spawn()` is used. * * The four broker outcomes in required mode: * { pid } → register the broker-owned process (unchanged); * { uncertain } → reconcileUncertainSpawn — never a blind local * spawn, so no timeout can yield two agents; * { error } | * { unavailable, → the supervisor is DOWN / crashed / restarting, * endpointAbsent:false } or answered a definitive spawn error. NO local * spawn — fail LOUDLY. This closes the * uncontained-zombie hole for the real failure * modes; the keeper retries the idempotent * broker contract on its next cycle; * { unavailable, → the ONLY Windows path that still local-spawns: * endpointAbsent:true } a definitively OLD supervisor whose /proc/spawn * endpoint is absent (404/401). One-release, * self-healing grace — WARN the operator to * re-run Install-Sensorium, then local-spawn * (uncontained until boot reconcile adopts it) * so an MCP update cannot strand agents behind * an old supervisor binary. * * In the local path stdout/stderr go to a file descriptor the CHILD owns, not * a pipe drained by this (ephemeral) server process. A pipe's only reader is * this node process; when it dies on a self-update/restart the orphaned, * detached child keeps writing, the ~64KB OS pipe buffer fills, and the next * write() blocks the child's event loop forever — killing MCP polling, the API * stream and any reconnect (the exact zombie we saw on thread 20869). A file * fd is duplicated into the child at spawn and drained by the OS, so it * survives our restarts. The broker path keeps the identical design with the * supervisor opening the file handle (spawn-to-file-fd, plan §6 "keep"). */ export declare function launchAgent(plan: AgentLaunchPlan, threadLifecycle: ThreadLifecycleService): Promise; //# sourceMappingURL=spawn-common.d.ts.map