import type { CodexOwnerSource } from "./services/sessions/codexRolloutOwner"; import { type BusySignal } from "./services/sessions/conversationBusy"; import type { ManagedSession, ServerConfig, SessionResponse } from "./types"; export declare const GRACE_MAX_DEFERS = 4; export declare const IDLE_REAP_AFTER_MS: number; export declare const IDLE_REAP_SWEEP_MS: number; export { ADOPT_KILL_TIMEOUT_MS, RESUME_DISCOVERY_TIMEOUT_MS, waitForProcessExit, } from "./api/handlers/sessions.handlers"; export { EXTERNAL_ACTIVE_WRITING_MS, EXTERNAL_TAIL_IDLE_MS, EXTERNAL_TAIL_MAX, EXTERNAL_TAIL_RECENCY_MS, } from "./external-tails"; export declare function parseIncludeAgentsEnv(raw: string | undefined): boolean; /** * Why a resume did not happen, in the vocabulary of the thing that failed * rather than of HTTP (plan Phase 7c). * * `resumeSession` has two callers with nothing in common at the response layer: * one writes a status code, the other writes a log line. Neither can be the * owner of these distinctions, so they live here. */ export type ResumeFailure = { ok: false; reason: "history_file_missing"; } | { ok: false; reason: "no_project_path"; } | { ok: false; reason: "conversation_busy"; /** Which signals fired — carried verbatim so the 409 body is unchanged. */ detectedBy: BusySignal[]; lastActivityMs: number | null; likelyOwner: "external" | "unknown"; } /** * Codex's own single-writer lock said no — either an open handle on the exact * rollout (pre-flight) or the `-32600` error Codex printed after spawn. * * Separate from `conversation_busy` because the two are not the same claim: * that one is a heuristic a caller may override with `force`, this one is the * provider refusing, which no flag of ours can bypass. */ | { ok: false; reason: "codex_session_active"; detectedBy: BusySignal[]; lastActivityMs: number | null; ownerPid?: number; ownerSource?: CodexOwnerSource; } /** Codex exited or errored during startup for some other reason. */ | { ok: false; reason: "codex_start_failed"; failureReason: string; }; export type ResumeOutcome = ResumeFailure | { ok: true; /** The session was already live here — nothing was spawned. */ alreadyRunning: true; session: null; response: SessionResponse; } | { ok: true; alreadyRunning: false; session: ManagedSession; /** Null only if the store lost the session between spawn and read. */ response: SessionResponse | null; }; export declare class StreamerServer { private httpServer; private ptyManager; private sessionStore; private wsHub; private fileWatcher; private sessionFileMap; private externalTails; private externalTailManager; private pendingLineSeqs; /** * A session's PTY size, recorded only when something resizes it away from the * spawn defaults. Absent means "still PTY_COLS x PTY_ROWS", which is why this * is a sparse map rather than a field on every session: resize is opt-in and * rare, and the default is what every reader already assumes. */ private sessionGeometry; private pendingQuestions; private contendedSessions; private selfPtyEndedAt; private pendingQuestionKey; private pendingPermission; private pendingPermissionKey; private promptRegistry; private scannerManager; private sessionWatchers; private registryBoot; private conversationHandlers; private sessionHandlers; private binding; private activeWarmups; private nextWarmupId; private inFlightCacheWrites; private apiKey; private apiKeySource; private localNoAuth; private logMenubarRequests; private verbose; private scanProfiles; private dbPool; private dbInstanceId; private disableDb; private host; private skipStartupWarmup; private autoResumeOnBoot; private browseRoot; private publicUrl; private browserCors; private pairTokens; private exchangeAttempts; private sessionStartAttempts; private sessionInputAttempts; private ptyGracePeriodMs; private defaultSystemPrompt; private featureFlags; private featureFlagSources; private codexSystemPromptEnabled; private defaultPermissionMode; private defaultModel; private defaultEffort; private claudeFlags; private claudeExtraArgs; private claudeFlagsPersistable; private ptyGraceTimers; private ptyGraceDeferCounts; private holdWhenIdle; private sessionSubscribers; private lastAgentChunkAt; private terminalSeq; private idempotency; private sessionVerdicts; private idleReaperTimer; private codexFormatCanaryTimer; private clientIdToWs; private wsToClientId; private cache; private cacheMonitor; private hostPressureMonitor; private projectsRepo; private conversationsRepo; private sessionsRepo; private managedSessionsRepo; private runtimeStore; private readonly streamerInstanceId; private cacheMetadataRepo; private pushRepo; private devicesRepo; private apnsClient; private liveActivityNotifier; private liveActivityRenewal; private waitingInputNotifier; private expoPushSender; private discoveryCache; private discoveryInFlight; private cacheDir; private runtimeDbPath; private tailSize; private directoryDebounceMs; private codexRoots; private cursorRoots; private includeAgents; private agentEntrypoints; private honoApp; private log; private agentConfig; private agentClient; private sessionStatusBus; constructor(config: ServerConfig & { apiKey: string; }); private ptyAttachedIds; private rememberSelfPtyEnded; /** * Send a session_list to only the client that triggered this HTTP request * (identified by X-Client-Id header → registered WS socket). Falls back to * a full broadcast if no match exists (old clients, or no WS registered yet). */ private broadcastOrUnicastSessionList; private sessionListPayload; /** * Overlay boot-reconciliation verdicts onto session responses. * * A session left by a previous run is not in the in-memory store, so * SessionStore cannot classify it — it only ever sees what this run spawned. * Discovery may still surface the process, in which case the reconciler knows * strictly more about it than discovery does: it can tell `detached` (alive * and confirmed ours) from `orphaned` (alive but identity unconfirmed), which * a pid enumeration alone cannot. * * Only applied when the session is NOT live here: a session this run owns has * an authoritative lifecycle already, and a stale verdict must never override * it. */ private withReconciledLifecycle; private addSessionSubscriber; private removeSessionSubscriber; /** * Bring up Live Activity push, if credentials are present (Feature 12). * * APNS_KEY absent is the ordinary case on a dev machine and in CI, so this * logs once at info and leaves the feature off rather than failing: the server * must not refuse to boot over a missing optional push credential. * * The key is read from the environment as PEM contents and never from a path * on disk; neither it nor any device token is ever logged. */ private initLiveActivityPush; /** * Bring up "your turn" notifications over Expo's relay (#528). * * Unconditional, unlike Live Activity push: Expo holds the app's APNs and FCM * credentials, so a self-hosted streamer needs no credential of its own. The * access token is optional and only relevant if the Expo project has enhanced * security enabled — requiring one would lock out every self-hoster, since * they do not own the project. It is never logged. */ private initWaitingInputPush; /** Whether any live socket is subscribed to this session — "someone is looking". */ private hasSessionSubscriber; /** * Log any Codex rollout assumption that no longer holds. * * Deliberately log-only. A format change is not something the streamer can * repair or route around, and a WS alert or a blocked request would turn an * informational signal into an outage. What it buys is that the next silent * empty conversation is preceded by a line naming the assumption that moved. */ private runCodexFormatCanary; /** * Release PTYs whose agent has been silent past IDLE_REAP_AFTER_MS. * * This is the bound that lets handleWsClose stop arming kill timers. The * distinction that matters: the old timer measured how long nobody was * *watching*, which is uncorrelated with whether work is in flight. This * measures how long the *agent* has produced nothing, and only ever considers * sessions that are already settled — a `running` PTY is skipped regardless of * age, so a long silent turn is never interrupted. * * Exposed (not private) so tests can drive one sweep deterministically instead * of waiting on the interval. */ reapIdleSessions(now?: number): string[]; private startGraceTimer; private clearGrace; /** * Arm the in-app "Kill on idle" latch: hold now if already settled, otherwise * on the next running → waiting_input (or idle). No grace delay, no defer cap. * A subscribed leaving socket must not block an immediate hold. */ private armHoldWhenIdle; /** * Fire the Kill-on-idle latch from the shared onStatusChange funnel. * Delete first so the ensuing idle transition cannot re-enter. */ private maybeFireHoldWhenIdle; private forgetIfEmptyUnused; private softDeleteConversation; get port(): number; private currentWarmupState; private beginWarmup; private finishWarmup; private withWarmup; private rejectIfWarmingUp; /** * Say which provider CLIs this machine can actually launch. * * The operator cannot discover this case unaided: under launchd/Task * Scheduler the service inherits a stripped PATH, so a CLI that works * perfectly in their terminal is invisible to the service, and every session * start dies milliseconds in. `/api/diagnostics` answers it too, but only for * someone who already suspects it. * * Availability only, never a version — `--version` costs a process spawn per * provider (85ms for claude here) and belongs on the first request that wants * it, not on boot. * * Called AFTER the port is bound, which is not cosmetic. This is the first * caller of the exe resolvers in the process, so the memo is cold by * definition and each provider pays one synchronous `which` / `where.exe` * (platform.ts) with a 3s timeout. On POSIX that is 3ms found, 7ms missing. * Windows is the risk — `where.exe` is slower, `execFileSync` blocks the * event loop, and Task Scheduler's stripped PATH is exactly where a miss * pays the full timeout — so the worst case is ~6s of two blocking lookups. * After `listen()` that delays the first requests on a box that cannot start * a session anyway; before it, it would have delayed binding the port. */ private logProviderAvailability; listen(port: number, opts?: { awaitReady?: boolean; }): Promise; private bindWithRetry; private bindWithRetryLoop; private trackCacheWrite; close(): Promise; private handleRequest; private handlePairStart; private handlePairExchange; private rotateApiKey; /** * The registry ships with the values so a client renders the list from one * round-trip, same as getClaudeFlagsConfig(). * * Deliberately no `persisted` field: unlike claude-flags there is no PUT, and * the absence of that field is the signal that this endpoint is read-only. */ /** * The boot warning for a run that turned transport encryption off from any * explicit, non-default source. * * D-8 wanted no env var to hold encryption off invisibly; that guarantee * turned out to be unenforceable without breaking the registry's uniform * five-rung resolution (see dilemmas.md D-8's resolution note), so this is * the replacement: every explicit "off" — `override`, `env`, `cli`, `yaml` — * announces itself the same way `--no-e2ee` always has. `default` is exempt: * off-by-default is not a decision anyone made this boot, and warning on * every default boot before stage 2 flips it would be noise with nothing * behind it. * * Fires regardless of the pinned count, including at zero — a leading * indicator (warn the moment the box boots plaintext) beats a lagging one * (warn only once a device has already paired against it). * * Through the console dest as well as the JSON log, because the person who * set the source is watching a terminal, and a warning they have to grep for * is a warning that arrives after the incident. It says what is readable and * by whom — the tunnel's edge terminates TLS, so "we are behind HTTPS" is not * an answer — and how many paired devices this run will refuse. * * Called after `devicesRepo` opens, since the count is the point. Silent when * the repo did not open: a boot already shouting about a failed runtime store * does not need a second line saying it also cannot count. */ private warnIfE2eeDisabled; /** * Ask this server's own public URL what an unauthenticated device would get. * * Fire-and-forget, deliberately: the probe must never delay `listen()` or * refuse a boot. It reports a misconfiguration the operator can fix, and a * server that will not start is a worse outcome than one that warns. * * Silent unless there is something to say. No public URL means no edge to * probe; e2ee off means no device would be refused by one; an unreachable URL * is a different problem with its own symptoms, and warning about it here * would cry wolf on every laptop that is merely offline. */ private probeAccessGate; private getFeatureFlagsConfig; private getClaudeFlagsConfig; /** * Replace the per-server flag set. Applies to the NEXT spawn — a live PTY * keeps the argv it was started with. * * Mirrors rotateApiKey(): when the values were pinned by a CLI flag we still * apply them in memory but skip the server.yaml write, because the flag would * win again on restart and silently revert them. * * Logged with old→new at info level on purpose: this can disable the * permission prompts entirely, so it needs a forensic trail. */ private setClaudeFlagsConfig; /** * The three spawn options that a configured claude-flag can override, with * the boot-time CLI/yaml default as the fallback. Spread into every * start/resume/adopt call so all three paths agree. * * These ids are excluded from buildFlagArgs (SPAWN_POSITIONAL_FLAG_IDS) * precisely because they arrive here instead — the PTY spawn paths pass them * as explicit positionals, so emitting them from the allowlist too would * duplicate the flag. * * Narrowed with the type guards rather than cast: ClaudeFlagValues is a loose * Record by design, and while validateFlagValues already guarantees the shape * on the way in, TypeScript cannot see that through the record. */ private spawnFlagOverrides; private checkRateLimit; private checkExchangeRateLimit; private checkSessionStartRateLimit; private checkSessionInputRateLimit; private handleSessionsCount; /** * Live Codex sessions keep `SessionResponse.conversationId === managed.id` * (stable deep-link / PTY key) and store the rollout UUID separately as * `boundConversationId`. REST history is indexed under the rollout UUID, so * resolve the placeholder → bound id before looking up the scanner. */ private resolveConversationLookupId; /** File path for a live managed session (placeholder id or bound Codex id). */ private findLiveSessionFilePath; /** True when a conversation UUID is the bound rollout of a live PTY session. */ private isBoundConversationLive; /** * Broadcast conversation JSONL lines to WS clients. Codex rollout and Cursor * transcript lines are normalized to the Claude `type:user|assistant` shape * mobile understands; Claude lines pass through unchanged. */ private broadcastConversationLines; /** * Resolve a client-supplied session/conversation id into everything needed to * launch against it: the id the PROVIDER filed the history under, that * history's path, the project cwd, and which CLI owns it. * * Shared by resume and fork so the two can never disagree about identity — * which for Codex is the whole difficulty: the id a client navigated to may * be a local placeholder, and only the registry knows the rollout id behind * it. */ private resolveConversationTarget; /** * Block until a freshly spawned session reaches `waiting_input` (ready) or * `idle` (failed), or until `timeoutMs` elapses with the process still alive. * * "timeout" is not an error: it is the pre-existing asynchronous contract — * the session keeps booting and the caller answers with a pending shape. */ private waitForStartupOutcome; /** * Drop every trace of a managed session: in-memory store, durable registry * row, and the collision-probe markers that would otherwise outlive it. * * Used when a start never became usable (`abandonFailedStart`) and when stop * is asked to discard an empty session that has no cached conversation. The * registry delete is load-bearing — `rehydrateSessions` will bring the row * back on the next boot if it remains. * * Callers that kill the PTY (`putOnHold`) must do that *first*: onStatusChange * on idle writes `selfPtyEndedAt` and a registry status, and those have to * be cleared here afterwards. */ private forgetSession; /** * Drop every trace of a session that never became usable. * * The runner has already torn itself down (failStartup / handleExit); what * remains is server-side bookkeeping that would otherwise leave a dead * session in the list, a registry row claiming a spawn, and a `selfPtyEndedAt` * marker that would suppress the mtime collision signal on the NEXT resume — * i.e. it would help hide the very owner we just collided with. */ private abandonFailedStart; private enrichResumedSessionAsync; private processJsonlQuestions; private cancelPendingQuestion; private handleBrowse; private handleMkdir; /** * Retarget a LIVE session's model or effort by typing the corresponding * Claude Code slash command into its PTY. * * There is no CLI or IPC channel for this — `--model`/`--effort` are spawn * arguments — so the interactive `/model ` / `/effort ` commands are the * only way to change a session already running. Both accept an argument and * apply it without opening the picker (verified against Claude Code v2.1.220). * * Answers 202, not 200: the value is applied by the TUI on its next render, so * there is nothing truthful to echo back synchronously. Clients confirm with * `GET /api/sessions/:id`, which scrapes the applied value off the live status * line. */ private applyLiveSessionSetting; } //# sourceMappingURL=server.d.ts.map