/** * Notebook Operation Router * * Routes notebook operations to either: * 1. Connected UI via WebSocket (real-time collaboration) * 2. Headless notebook manager (file-based when no UI) * * From the agent's (MCP) perspective, operations look identical regardless of mode. */ import { WebSocket } from 'ws'; import { HeadlessOperationHandler } from './headless-handler'; interface KernelChangedPayload { kernelName: string; serverId?: string | null; mtime?: number; } type Backend = 'ui' | 'headless'; interface OperationResult { success: boolean; error?: string; backend?: Backend; [key: string]: unknown; } interface AgentLock { agentId: string; clientName?: string; clientVersion?: string; expiresAt: number; notebookPath: string; lockedAt: number; exclusive: boolean; cellHashes: Map; } export declare class OperationRouter { private uiConnections; private headlessHandler; private operationTimeout; private agentLocks; private preSessionReadHashes; setHeadlessHandler(handler: HeadlessOperationHandler): void; /** * Register a UI connection for a notebook path. */ registerUI(websocket: WebSocket, notebookPath: string): Promise; /** * Unregister a UI connection. */ unregisterUI(websocket: WebSocket, notebookPath: string): void; notifyKernelChanged(notebookPath: string, payload: KernelChangedPayload): void; /** * Check if a UI is connected for the notebook. */ hasUI(notebookPath: string): boolean; /** * Record UI WebSocket activity so stale connections stop intercepting operations. */ markUIActivity(websocket: WebSocket, notebookPath: string): void; /** * Start an agent session with locking. * Returns success if lock acquired, error if already locked by another agent. */ startAgentSession(notebookPath: string, agentId: string, metadata?: { clientName?: string; clientVersion?: string; exclusive?: boolean; }): { success: boolean; error?: string; lock?: AgentLock; }; /** * End an agent session and release the lock. * Only the lock holder can release the lock. */ endAgentSession(notebookPath: string, agentId: string): { success: boolean; error?: string; }; /** * Check if a notebook is in an agent session. */ isAgentSession(notebookPath: string): boolean; /** * Get the agent ID holding the lock, if any. */ getAgentLock(notebookPath: string): AgentLock | null; /** * Refresh lock timeout for an agent operation. */ refreshAgentLock(notebookPath: string, agentId: string): boolean; /** * Clean up expired locks. */ private cleanupExpiredLocks; /** * Get the first available UI connection (for operations that can use any UI). */ private getAnyUIConnection; private isUIConnectionResponsive; private getResponsiveUIConnection; /** * Apply a notebook operation. */ applyOperation(operation: Record): Promise; /** * OCC preparation for a destructive write in a collaborative session. * Returns an error string to reject the operation, or null to proceed * (with `expectedHash`/`expectedHashes` stamped onto the operation). * * Policy: * - updateContent / updateMetadata / deleteCell / deleteCells: the agent * must have read the cell this session (hash known), and the applier * verifies the content still matches before applying. * - executeCell: verified only when the hash is known (running slightly * stale content is recoverable; destroying user edits is not). * - clearNotebook: requires an exclusive session. * - Index-addressed destructive writes are rejected: user edits shift * indices, so collaborative writes must address cells by id. */ private prepareCollaborativeWrite; /** * Record the session's view of cell content from operation results. * Keyed to the notebook's active lock: the lock guarantees a single writing * agent per notebook, so reads from any source refreshing it is sound. */ private recordSessionHashes; /** * Record hashes for a full-notebook read (used by router.readNotebook, * which serves the MCP read_notebook tool outside applyOperation). */ recordNotebookReadHashes(notebookPath: string, cells: Array<{ id?: string; content?: string; }>): void; private getPreSessionStore; private forwardToUI; /** * Handle operation response from UI. */ handleUIResponse(notebookPath: string, response: Record): void; private applyHeadless; /** * Read notebook state. */ readNotebook(notebookPath: string, includeOutputs?: boolean, maxLines?: number, maxChars?: number, maxLinesError?: number, maxCharsError?: number): Promise; private readFromUI; private readFromFile; private applyOutputTruncation; } export declare const operationRouter: OperationRouter; export {};