/** * Kernel Service * * Manages Jupyter kernel sessions - spawning, execution, and lifecycle. * Uses ZeroMQ for kernel communication following the Jupyter messaging protocol. */ import { KernelOutput, ExecutionResult, ExecutionQueueInfo, InternalExecutionOptions, StartKernelOptions, SessionInfo, KernelServiceConfig } from './types'; import { SessionStore } from './session-store'; import { KernelSpec } from './kernelspec'; /** Kernel-originated comm event surfaced to onComm listeners. */ export interface CommEvent { msgType: 'comm_open' | 'comm_msg' | 'comm_close'; commId: string; targetName?: string; data: Record; /** Binary buffer frames, base64-encoded. Present only when non-empty. */ buffers?: string[]; } /** Resolved by BoundedAsyncQueue.next() when the queue has been closed. */ export declare const IOPUB_QUEUE_CLOSED: unique symbol; /** Resolved by BoundedAsyncQueue.next(timeoutMs) when the wait timed out. */ export declare const IOPUB_QUEUE_TIMEOUT: unique symbol; /** * Polyfill IPython-style `!command` lines for kernels whose language lacks * them, by rewriting each such line into the language's own shell-call idiom * before the code reaches the kernel. Because the rewritten code still runs * in the kernel process, cwd, environment (conda env, loaded modules), and * machine (allocation node) are all the kernel's — the same guarantees * IPython's native `!` gives Python users. * * Python kernels are deliberately untouched: IPython's `!` is richer than a * plain shell call (output capture `x = !ls`, `{var}` interpolation, * streaming output) and a rewrite would break it. * * Only whole lines that start with `!` are rewritten. Known limits (v1): * a line inside a multi-line string that happens to start with `!` would be * rewritten too, and R's intern=TRUE buffers output until the command exits. */ export declare function rewriteShellLines(code: string, language: string | undefined): string; export declare class KernelService { private sessions; private fileToSession; private kernelProcesses; private processLifecycleHandlers; private zmqSockets; private sessionStore; private config; private kernelSpecsCache; private ready; private zmq; private executionQueues; private executionQueueSizes; private shellRequestQueues; private reattachInProgress; private serverId; private serverInstanceId; private legacyCleanupWarned; private cellOutputBuffers; private cellOutputTracking; private executingCellIds; private iopubReaders; private iopubParentSubscribers; private iopubCatchAllSubscribers; private commStates; private shellReaders; private shellReplyWaiters; constructor(config?: KernelServiceConfig, sessionStore?: SessionStore); private deadListeners; onSessionDead(cb: (sessionId: string) => void): void; private notifySessionDead; private commListeners; onComm(cb: (sessionId: string, comm: CommEvent) => void): void; private notifyComm; private livenessTimer; private startLivenessSweep; /** One liveness pass (exposed for tests; normally driven by the timer). */ sweepLiveness(): void; setServerIdentity(serverId: string, serverInstanceId?: string): void; /** * Check if a PID is still alive. */ private isPidAlive; private attachProcessLifecycle; private detachProcessLifecycle; /** * Load a Jupyter connection file. */ private loadConnectionFile; /** * Initialize the service (lazy load ZeroMQ and discover kernels) */ initialize(): Promise; /** * Attempt to reattach to orphaned kernel sessions from a previous server run. */ reattachOrphanedSessions(): Promise<{ attempted: number; reattached: number; failed: number; skipped: number; }>; /** * Check if service is ready */ get isReady(): boolean; /** * Get available kernelspecs */ getAvailableKernels(): KernelSpec[]; /** * Re-run kernelspec discovery and refresh the in-memory cache. * * `getAvailableKernels()` caches once at startup and never expires, so a kernel * registered while the server is running would otherwise stay invisible until a * restart. Call this after registering/installing a kernel. */ refreshKernelSpecs(): KernelSpec[]; /** * Normalize file path for consistent lookup */ private normalizePath; /** * Normalize notebook path for external callers (e.g., routes). */ normalizeNotebookPath(filePath: string): string; /** * Save kernel preference for a notebook file. */ saveNotebookKernelPreference(filePath: string, kernelName: string, serverId?: string | null): void; /** * Get kernel preference for a notebook file. */ getNotebookKernelPreference(filePath: string): { kernelName: string; serverId: string | null; updatedAt: number; } | null; /** * Generate a connection file for the kernel */ private allocateEphemeralPort; private generateConnectionFile; /** * Start a new kernel session */ startKernel(options?: StartKernelOptions): Promise; /** * Wait for kernel to be ready by connecting to ZeroMQ channels. * * Returns the detected kernel execution state: * - 'idle': kernel responded to kernel_info_request (ready for work) * - 'busy': kernel PID is alive but shell channel is blocked (mid-execution) * * @param timeoutSeconds Optional override for startup timeout (default: use config) * @param pid Optional kernel PID — used to distinguish "busy" from "dead" on timeout */ private waitForReady; private startIopubReader; private stopIopubReader; private closeIopubSubscribers; private dispatchIopubMessage; /** * Subscribe to iopub messages parented by a specific msg_id. Must be called * BEFORE sending the request so no reply can slip past the demux. Callers * must unsubscribe in a finally block. */ private subscribeIopubParent; private unsubscribeIopubParent; /** Subscribe to ALL iopub messages for a session (any parent). */ private subscribeIopubCatchAll; private unsubscribeIopubCatchAll; private startShellReader; private stopShellReader; private closeShellReplyWaiters; /** * Register a one-shot waiter for the shell reply parented by msgId. Must be * called BEFORE sending the request so the reply cannot slip past the * dispatch. Resolves 'closed' immediately if no reader loop is alive, or * later when the reader stops (kernel stop/restart/cleanup). */ private registerShellReplyWaiter; private removeShellReplyWaiter; /** * Await a previously registered shell reply with a timeout. The waiter is * always deregistered on the way out (timeout, reply, or error), so a late * reply is dropped by the reader instead of leaking a waiter. */ private waitForShellReply; private handleKernelCommMessage; private rememberOpenComm; /** * Open comms known for a session (for late-joining clients). * Returns comm_id -> { targetName, openData } where openData is the last * state-carrying comm_open payload observed for that comm. */ getOpenComms(sessionId: string): Record; }>; /** * Send a comm message (comm_open / comm_msg / comm_close) to the kernel on * the shell channel. Comm messages produce NO shell reply, so the queued * slot releases as soon as the send completes — we never block waiting for * a reply that will not come. * * @param buffers Optional binary buffer frames, base64-encoded. */ sendCommMessage(sessionId: string, msgType: 'comm_open' | 'comm_msg' | 'comm_close', content: Record, buffers?: string[]): Promise; /** * Monitor a busy reattached kernel on iopub. When its current execution * finishes (status: idle on iopub), verify shell connectivity and update * the in-memory session status. */ private monitorBusyKernel; /** * Send kernel_info_request and wait for reply to verify kernel is ready * Throws if no reply received within timeout */ private sendKernelInfoRequest; /** * Create a Jupyter protocol message */ private createJupyterMessage; /** * Get or create kernel for a file (one notebook = one kernel). * Returns whether a new session was created. */ /** In-flight create/attach per normalized file path (single-flight). * Without this, two near-simultaneous requests (UI re-render + agent op, * double-click) both miss the fileToSession check and spawn TWO kernel * processes — the second overwrites the mapping and the first leaks. */ private inflightKernelCreates; getOrCreateKernel(filePath: string, kernelName?: string): Promise<{ sessionId: string; created: boolean; }>; private getOrCreateKernelInternal; /** * Get existing kernel session ID for a notebook file (if any). * Returns null if no live session is associated with the file. */ getSessionIdForFile(filePath: string): string | null; /** * Check if a kernel session exists. */ hasSession(sessionId: string): boolean; /** * Get the notebook file path associated with a session (if any). * Used for output persistence when no UI is connected. */ getSessionFilePath(sessionId: string): string | null; /** * Get the kernel name associated with a session (if any). */ getSessionKernelName(sessionId: string): string | null; /** * Execute code in a kernel session */ executeCode(sessionId: string, code: string, onOutput: (output: KernelOutput, cellId?: string | null) => Promise, onQueueInfo?: (info: ExecutionQueueInfo) => void, cellId?: string | null, internalOptions?: InternalExecutionOptions): Promise; private enqueueExecution; /** * Serialize shell socket SENDS. Receives are owned by the unified shell * reader, so tasks queued here must be send-only and release the slot as * soon as the send completes (register any reply waiter BEFORE enqueueing, * await the reply AFTER the slot is released). Holding the slot across a * reply wait would let one slow request delay every later send. */ private enqueueShellRequest; private reserveExecutionSlot; private releaseExecutionSlot; private formatExecutionError; private executeCodeInternal; /** * Request code completion from the kernel * Uses a separate queue for shell socket operations to avoid "socket busy" errors */ complete(sessionId: string, code: string, cursorPos: number): Promise<{ status: string; matches: string[]; cursor_start: number; cursor_end: number; }>; /** * Internal completion implementation. The send is serialized through the * shell queue; the reply is awaited via the unified shell reader, so a * timeout here simply drops the waiter and can never block or swallow a * later request's reply. */ private completeInternal; /** * Parse a Jupyter protocol message */ private parseJupyterMessage; /** * Format display data for output */ private formatDisplayData; /** * Strip ANSI escape codes from text */ private stripAnsi; private getOutputStats; private ensureCellBuffers; private ensureCellTracking; /** * Buffer an output for a cell. Returns the outputs actually stored (0 if truncated, 1 normally). */ bufferOutput(sessionId: string, output: KernelOutput, cellId?: string | null): KernelOutput[]; /** * Clear outputs for a specific cell (called on re-execute). */ clearCellOutputs(sessionId: string, cellId: string): void; /** * Get all cell outputs for a session, grouped by cellId. */ getAllCellOutputs(sessionId: string): Map; getExecutingCellId(sessionId: string): string | null; /** * Stop a kernel session */ stopKernel(sessionId: string): Promise; /** * Check command line for a PID (best-effort). */ private getPidCommand; /** * Get process start time (best-effort), used to prevent PID reuse mistakes. */ private getProcessStartTime; /** * Best-effort check to ensure the PID is a kernel process we started. */ private isExpectedKernelProcess; /** * Attempt to terminate a PID gracefully, then force kill. */ private terminatePid; private cleanupPersistedSessionArtifacts; /** * Cleanup kernel resources */ private cleanupKernelResources; /** * Cleanup only in-memory tracking for a session (keeps connection file and session store). */ private cleanupInMemorySession; /** * Interrupt kernel execution */ interruptKernel(sessionId: string): Promise; /** * Restart a kernel (in-place, preserving session ID like Python) */ restartKernel(sessionId: string): Promise; /** * Get session status */ getSessionStatusFast(sessionId: string): SessionInfo | null; getSessionStatus(sessionId: string): Promise; /** * Activity snapshot for the idle auto-release monitor (client mode): * whether any kernel is busy/starting, and the most recent kernel activity * across sessions in ms since epoch (sessions track it in seconds). */ getIdleSnapshot(): { anyBusy: boolean; lastActivityMs: number | null; }; /** * Get all sessions */ getAllSessions(): Promise; /** * Get dead sessions (orphaned or terminated) that can be cleaned up */ getDeadSessions(): { sessionId: string; kernelName: string; filePath: string | null; status: string; lastHeartbeat: number; }[]; /** * Auto-delete dead session rows whose kernel process is CONFIRMED gone * (no PID, PID not running, or PID reused by another process). These are * pure bookkeeping — nothing to kill, nothing to lose — so they need no * user confirmation. Rows whose PID is still alive are kept for the * explicit "Clean Up" flow (killing a process should stay a user action, * and legacy rows without a start-time fingerprint can't be verified). */ autoCleanupDeadSessions(): Promise; /** * Cleanup dead sessions by deleting them from the database */ cleanupDeadSessions(sessionIds?: string[]): Promise; /** * Cleanup all sessions */ cleanup(): Promise; /** * Shutdown kernel service. * If preserveKernels is true, keep kernel processes running and close the session store. */ shutdown(options?: { preserveKernels?: boolean; }): Promise; /** * Helper: sleep for ms milliseconds */ private sleep; /** * Get memory usage (RSS) for multiple processes in MB * Returns a map of pid -> memoryMb * Cross-platform: uses /proc on Linux, ps on macOS */ private getProcessMemoryMap; } export declare const kernelService: KernelService;