pi-app-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/dist/command-classification.d.ts +59 -0
  4. package/dist/command-classification.d.ts.map +1 -0
  5. package/dist/command-classification.js +78 -0
  6. package/dist/command-classification.js.map +7 -0
  7. package/dist/command-execution-engine.d.ts +118 -0
  8. package/dist/command-execution-engine.d.ts.map +1 -0
  9. package/dist/command-execution-engine.js +259 -0
  10. package/dist/command-execution-engine.js.map +7 -0
  11. package/dist/command-replay-store.d.ts +241 -0
  12. package/dist/command-replay-store.d.ts.map +1 -0
  13. package/dist/command-replay-store.js +306 -0
  14. package/dist/command-replay-store.js.map +7 -0
  15. package/dist/command-router.d.ts +25 -0
  16. package/dist/command-router.d.ts.map +1 -0
  17. package/dist/command-router.js +353 -0
  18. package/dist/command-router.js.map +7 -0
  19. package/dist/extension-ui.d.ts +139 -0
  20. package/dist/extension-ui.d.ts.map +1 -0
  21. package/dist/extension-ui.js +189 -0
  22. package/dist/extension-ui.js.map +7 -0
  23. package/dist/resource-governor.d.ts +254 -0
  24. package/dist/resource-governor.d.ts.map +1 -0
  25. package/dist/resource-governor.js +603 -0
  26. package/dist/resource-governor.js.map +7 -0
  27. package/dist/server-command-handlers.d.ts +120 -0
  28. package/dist/server-command-handlers.d.ts.map +1 -0
  29. package/dist/server-command-handlers.js +234 -0
  30. package/dist/server-command-handlers.js.map +7 -0
  31. package/dist/server-ui-context.d.ts +22 -0
  32. package/dist/server-ui-context.d.ts.map +1 -0
  33. package/dist/server-ui-context.js +221 -0
  34. package/dist/server-ui-context.js.map +7 -0
  35. package/dist/server.d.ts +82 -0
  36. package/dist/server.d.ts.map +1 -0
  37. package/dist/server.js +561 -0
  38. package/dist/server.js.map +7 -0
  39. package/dist/session-lock-manager.d.ts +100 -0
  40. package/dist/session-lock-manager.d.ts.map +1 -0
  41. package/dist/session-lock-manager.js +199 -0
  42. package/dist/session-lock-manager.js.map +7 -0
  43. package/dist/session-manager.d.ts +196 -0
  44. package/dist/session-manager.d.ts.map +1 -0
  45. package/dist/session-manager.js +1010 -0
  46. package/dist/session-manager.js.map +7 -0
  47. package/dist/session-store.d.ts +190 -0
  48. package/dist/session-store.d.ts.map +1 -0
  49. package/dist/session-store.js +446 -0
  50. package/dist/session-store.js.map +7 -0
  51. package/dist/session-version-store.d.ts +83 -0
  52. package/dist/session-version-store.d.ts.map +1 -0
  53. package/dist/session-version-store.js +117 -0
  54. package/dist/session-version-store.js.map +7 -0
  55. package/dist/type-guards.d.ts +59 -0
  56. package/dist/type-guards.d.ts.map +1 -0
  57. package/dist/type-guards.js +40 -0
  58. package/dist/type-guards.js.map +7 -0
  59. package/dist/types.d.ts +621 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +23 -0
  62. package/dist/types.js.map +7 -0
  63. package/dist/validation.d.ts +22 -0
  64. package/dist/validation.d.ts.map +1 -0
  65. package/dist/validation.js +323 -0
  66. package/dist/validation.js.map +7 -0
  67. package/package.json +135 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/server.ts"],
4
+ "sourcesContent": ["#!/usr/bin/env node\n/**\n * pi-app-server - Session multiplexer for pi-coding-agent\n *\n * Exposes N independent AgentSessions through dual transports:\n * - WebSocket on port 3141\n * - stdio (JSON lines)\n *\n * The protocol IS the architecture.\n */\n\nimport * as readline from \"readline\";\nimport { WebSocketServer, WebSocket } from \"ws\";\nimport { PiSessionManager } from \"./session-manager.js\";\nimport type { RpcCommand, RpcResponse, Subscriber, RpcBroadcast } from \"./types.js\";\nimport { getSessionId as getSessionIdFromCmd, isCreateSessionResponse } from \"./types.js\";\nimport { type AuthProvider, type AuthContext, AllowAllAuthProvider } from \"./auth.js\";\nimport {\n MetricsEmitter,\n type MetricsSink,\n NoOpSink,\n MemorySink,\n CompositeSink,\n MetricNames,\n ThresholdAlertSink,\n type ThresholdConfig,\n type Alert,\n} from \"./metrics-index.js\";\nimport { type Logger, ConsoleLogger, type LogLevel } from \"./logger-index.js\";\n\n// ============================================================================\n// CONSTANTS\n// ============================================================================\n\nconst SERVER_VERSION = \"0.1.0\";\nconst PROTOCOL_VERSION = \"1.0.0\";\nconst DEFAULT_PORT = 3141;\n\n/** Default graceful shutdown timeout (30 seconds) */\nconst DEFAULT_SHUTDOWN_TIMEOUT_MS = 30000;\n\n/** WebSocket backpressure threshold (64KB). Beyond this, we start dropping non-critical messages. */\nconst BACKPRESSURE_THRESHOLD_BYTES = 64 * 1024;\n\n/** WebSocket critical backpressure threshold (1MB). Beyond this, we close the connection. */\nconst BACKPRESSURE_CRITICAL_BYTES = 1024 * 1024;\n\n/** Stdio backpressure threshold (256KB). Beyond this, we start dropping non-critical messages. */\nconst _STDIO_BACKPRESSURE_THRESHOLD_BYTES = 256 * 1024;\n\n/** WebSocket heartbeat interval (30 seconds). */\nconst HEARTBEAT_INTERVAL_MS = 30 * 1000;\n\n/** WebSocket heartbeat timeout (10 seconds). If no pong received, close connection. */\nconst HEARTBEAT_TIMEOUT_MS = 10 * 1000;\n\n/**\n * WebSocket connection state for heartbeat tracking.\n */\ninterface WebSocketConnectionState {\n /** Whether we're waiting for a pong response. */\n waitingForPong: boolean;\n /** Timestamp of last pong received. */\n lastPongAt: number;\n /** Heartbeat interval timer. */\n heartbeatTimer: NodeJS.Timeout | null;\n /** Timeout timer for missing pong. */\n pongTimeoutTimer: NodeJS.Timeout | null;\n /** Whether the connection has been cleaned up (prevents use-after-free in async callbacks). */\n cleanedUp: boolean;\n}\n\n/**\n * Send result types for backpressure-aware WebSocket sends.\n */\ntype SendResult =\n | { ok: true }\n | { ok: false; reason: \"backpressure\" | \"closed\" | \"error\"; error?: Error };\n\n/**\n * Backpressure-aware WebSocket send.\n *\n * - Returns { ok: true } if sent successfully\n * - Returns { ok: false, reason: \"backpressure\" } if message dropped due to backpressure\n * - Returns { ok: false, reason: \"closed\" } if connection not open\n * - Returns { ok: false, reason: \"error\" } if send threw\n *\n * For critical messages (isCritical: true), we attempt send even under mild backpressure.\n * Under critical backpressure (>1MB), connection is closed to prevent OOM.\n */\nfunction sendWithBackpressure(\n ws: WebSocket,\n data: string,\n options: { isCritical?: boolean } = {}\n): SendResult {\n const { isCritical = false } = options;\n\n if (ws.readyState !== WebSocket.OPEN) {\n return { ok: false, reason: \"closed\" };\n }\n\n const buffered = ws.bufferedAmount;\n\n // Critical backpressure: close connection to prevent OOM\n if (buffered > BACKPRESSURE_CRITICAL_BYTES) {\n try {\n ws.close(1011, \"Server overloaded - backpressure critical\");\n } catch {\n // Ignore close errors\n }\n return { ok: false, reason: \"backpressure\" };\n }\n\n // Mild backpressure: drop non-critical messages\n if (buffered > BACKPRESSURE_THRESHOLD_BYTES && !isCritical) {\n return { ok: false, reason: \"backpressure\" };\n }\n\n try {\n ws.send(data);\n return { ok: true };\n } catch (error) {\n return {\n ok: false,\n reason: \"error\",\n error: error instanceof Error ? error : new Error(String(error)),\n };\n }\n}\n\n/**\n * Stdio state for backpressure tracking.\n */\ninterface StdioState {\n /** Whether stdout has backpressure (last write returned false). */\n hasBackpressure: boolean;\n /** Count of dropped messages due to backpressure. */\n droppedCount: number;\n /** Whether drain handler is registered. */\n drainHandlerRegistered: boolean;\n}\n\n/**\n * Backpressure-aware stdio send.\n *\n * - Returns true if sent successfully\n * - Returns false if dropped due to backpressure\n * - Critical messages are always attempted\n */\nfunction sendWithStdioBackpressure(\n data: string,\n state: StdioState,\n options: { isCritical?: boolean } = {}\n): boolean {\n const { isCritical = false } = options;\n\n try {\n const canWrite = process.stdout.write(data + \"\\n\");\n\n if (!canWrite) {\n state.hasBackpressure = true;\n\n // Register drain handler if not already registered\n if (!state.drainHandlerRegistered) {\n state.drainHandlerRegistered = true;\n process.stdout.once(\"drain\", () => {\n state.hasBackpressure = false;\n });\n }\n }\n\n return true;\n } catch (error) {\n // If write throws, stdout is broken\n if (isCritical) {\n // For critical messages, try to log to stderr as fallback\n console.error(`[stdio] Critical message failed:`, error);\n }\n return false;\n }\n}\n\n/**\n * Start heartbeat for a WebSocket connection.\n * Sends periodic pings and closes connection if pong not received.\n */\nfunction startHeartbeat(ws: WebSocket, state: WebSocketConnectionState): void {\n state.lastPongAt = Date.now();\n state.waitingForPong = false;\n state.cleanedUp = false;\n\n state.heartbeatTimer = setInterval(() => {\n // Check cleanup flag first to prevent use-after-free\n if (state.cleanedUp || ws.readyState !== WebSocket.OPEN) {\n stopHeartbeat(state);\n return;\n }\n\n // If still waiting for previous pong, close connection\n if (state.waitingForPong) {\n const elapsed = Date.now() - state.lastPongAt;\n console.error(`[WebSocket] No pong received after ${elapsed}ms, closing connection`);\n stopHeartbeat(state);\n try {\n ws.close(1001, \"Heartbeat timeout\");\n } catch {\n // Ignore close errors\n }\n return;\n }\n\n // Send ping\n state.waitingForPong = true;\n try {\n ws.ping();\n } catch {\n // Ping failed, connection likely dead\n stopHeartbeat(state);\n }\n\n // Set pong timeout\n state.pongTimeoutTimer = setTimeout(() => {\n // Check cleanup flag first to prevent race with cleanupConnection\n if (state.cleanedUp) return;\n\n if (state.waitingForPong && ws.readyState === WebSocket.OPEN) {\n console.error(\n `[WebSocket] Pong timeout after ${HEARTBEAT_TIMEOUT_MS}ms, closing connection`\n );\n stopHeartbeat(state);\n try {\n ws.close(1001, \"Heartbeat timeout\");\n } catch {\n // Ignore close errors\n }\n }\n }, HEARTBEAT_TIMEOUT_MS);\n }, HEARTBEAT_INTERVAL_MS);\n}\n\n/**\n * Stop heartbeat timers for a WebSocket connection.\n */\nfunction stopHeartbeat(state: WebSocketConnectionState): void {\n // Set cleanedUp flag first to prevent any in-flight callbacks from acting\n state.cleanedUp = true;\n if (state.heartbeatTimer) {\n clearInterval(state.heartbeatTimer);\n state.heartbeatTimer = null;\n }\n if (state.pongTimeoutTimer) {\n clearTimeout(state.pongTimeoutTimer);\n state.pongTimeoutTimer = null;\n }\n state.waitingForPong = false;\n}\n\n// ============================================================================\n// SERVER\n// ============================================================================\n\nexport interface PiServerOptions {\n /** Authentication provider (default: AllowAllAuthProvider) */\n authProvider?: AuthProvider;\n /** Metrics sink(s) for observability. Can be a single sink or CompositeSink. */\n metricsSink?: MetricsSink;\n /** Include MemorySink automatically for get_metrics command (default: true) */\n includeMemoryMetrics?: boolean;\n /** Logger for structured logging (default: ConsoleLogger with 'info' level) */\n logger?: Logger;\n /** Log level for default ConsoleLogger (ignored if logger is provided) */\n logLevel?: LogLevel;\n /** Alert thresholds for automatic monitoring (default: built-in thresholds) */\n alertThresholds?: Record<string, ThresholdConfig>;\n /** Called when an alert fires (default: console logging) */\n onAlert?: (alert: Alert) => void | Promise<void>;\n /** Called when an alert clears (optional) */\n onAlertClear?: (alert: Alert) => void | Promise<void>;\n}\n\nexport class PiServer {\n private sessionManager = new PiSessionManager();\n private wss: WebSocketServer | null = null;\n private stdinInterface: readline.Interface | null = null;\n private authProvider: AuthProvider;\n private serverStartTime = Date.now();\n private metrics: MetricsEmitter;\n private logger: Logger;\n /** Memory sink for get_metrics command (if included) */\n private memorySink: MemorySink | null = null;\n /** Stdio backpressure state */\n private stdioState: StdioState = {\n hasBackpressure: false,\n droppedCount: 0,\n drainHandlerRegistered: false,\n };\n\n constructor(options: PiServerOptions = {}) {\n this.authProvider = options.authProvider ?? new AllowAllAuthProvider();\n\n // Setup logger\n this.logger =\n options.logger ??\n new ConsoleLogger({\n level: options.logLevel ?? \"info\",\n component: \"pi-server\",\n });\n\n // Default alert thresholds for built-in monitoring\n const defaultAlertThresholds: Record<string, ThresholdConfig> = {\n [MetricNames.RATE_LIMIT_GENERATION_COUNTER]: {\n info: 1e12, // 1 trillion - start paying attention\n warn: 1e14, // 100 trillion - concerning\n critical: 1e15, // 1 quadrillion - action needed\n },\n };\n\n // Setup metrics system\n const includeMemory = options.includeMemoryMetrics ?? true;\n const sinks: MetricsSink[] = [];\n\n if (options.metricsSink) {\n sinks.push(options.metricsSink);\n }\n\n if (includeMemory) {\n this.memorySink = new MemorySink({ maxEvents: 1000 });\n sinks.push(this.memorySink);\n }\n\n // Create base composite sink (or no-op if no sinks)\n const baseSink = sinks.length > 0 ? new CompositeSink(sinks) : new NoOpSink();\n\n // Wrap with ThresholdAlertSink for monitoring\n const alertThresholds = options.alertThresholds ?? defaultAlertThresholds;\n const onAlert =\n options.onAlert ??\n ((alert: Alert) => {\n const levelStr = `[${alert.level.toUpperCase()}]`;\n if (alert.level === \"critical\") {\n console.error(`${levelStr} ${alert.message}`);\n } else {\n console.log(`${levelStr} ${alert.message}`);\n }\n });\n\n const alertSink = new ThresholdAlertSink({\n sink: baseSink,\n thresholds: alertThresholds,\n onAlert,\n onClear: options.onAlertClear,\n maxAlertStates: 1000,\n });\n\n this.metrics = new MetricsEmitter({ sink: alertSink });\n\n // Wire metrics to governor for rate limit monitoring\n this.sessionManager.getGovernor().setMetrics(this.metrics);\n\n // Wire memory metrics provider to session manager\n if (this.memorySink) {\n this.sessionManager.setMemoryMetricsProvider(() => this.memorySink!.getMetrics());\n }\n }\n\n async start(port: number = DEFAULT_PORT): Promise<void> {\n // Start WebSocket server\n this.wss = new WebSocketServer({ port });\n this.setupWebSocket(this.wss);\n\n await new Promise<void>((resolve, reject) => {\n const onListening = () => {\n this.wss?.off(\"error\", onError);\n resolve();\n };\n const onError = (error: Error) => {\n this.wss?.off(\"listening\", onListening);\n reject(error);\n };\n\n this.wss?.once(\"listening\", onListening);\n this.wss?.once(\"error\", onError);\n }).catch((error) => {\n throw new Error(\n `Failed to start WebSocket server on port ${port}: ${error instanceof Error ? error.message : String(error)}`\n );\n });\n\n // Setup stdio transport\n this.stdinInterface = this.setupStdio();\n\n // ADR-0007: Start periodic session metadata cleanup (every hour)\n this.sessionManager.startSessionCleanup(3600000);\n\n // Start periodic rate limit timestamp cleanup (every 5 minutes)\n this.sessionManager.getGovernor().startPeriodicCleanup(300000);\n\n // Broadcast server_ready\n const readyEvent: RpcBroadcast = {\n type: \"server_ready\",\n data: {\n serverVersion: SERVER_VERSION,\n protocolVersion: PROTOCOL_VERSION,\n transports: [\"websocket\", \"stdio\"],\n },\n };\n this.sessionManager.broadcast(JSON.stringify(readyEvent));\n\n // Record server start metric\n this.metrics.event(MetricNames.EVENT_SESSION_CREATED, { event: \"server_ready\" });\n\n this.logger.info(\"Server started\", {\n version: SERVER_VERSION,\n protocol: PROTOCOL_VERSION,\n port,\n transports: [\"websocket\", \"stdio\"],\n });\n }\n\n /**\n * Check if server is shutting down.\n */\n isInShutdown(): boolean {\n return this.sessionManager.isInShutdown();\n }\n\n /**\n * Get session manager for external access.\n */\n getSessionManager(): PiSessionManager {\n return this.sessionManager;\n }\n\n /**\n * Get metrics emitter for external access.\n */\n getMetrics(): MetricsEmitter {\n return this.metrics;\n }\n\n /**\n * Get logger for external access.\n */\n getLogger(): Logger {\n return this.logger;\n }\n\n /**\n * Get memory sink metrics (for get_metrics command).\n * Returns undefined if includeMemoryMetrics was false.\n */\n getMemoryMetrics(): Record<string, unknown> | undefined {\n return this.memorySink?.getMetrics();\n }\n\n /**\n * Graceful shutdown.\n * 1. Stop accepting new connections\n * 2. Broadcast shutdown notification\n * 3. Drain in-flight commands\n * 4. Close all WebSocket connections\n * 5. Close stdin\n * 6. Dispose all sessions\n */\n async stop(timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS): Promise<void> {\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n timeoutMs = DEFAULT_SHUTDOWN_TIMEOUT_MS;\n }\n\n // Check via session manager (single source of truth)\n if (this.sessionManager.isInShutdown()) {\n return; // Already shutting down\n }\n\n this.logger.info(\"Graceful shutdown initiated\");\n\n // ADR-0007: Stop periodic cleanup\n this.sessionManager.stopSessionCleanup();\n\n // Stop periodic rate limit timestamp cleanup\n this.sessionManager.getGovernor().stopPeriodicCleanup();\n\n // Dispose auth provider\n if (this.authProvider.dispose) {\n try {\n await Promise.resolve(this.authProvider.dispose());\n } catch (error) {\n this.logger.logError(\n \"Auth provider dispose failed\",\n error instanceof Error ? error : new Error(String(error))\n );\n }\n }\n\n // Stop accepting new WebSocket connections\n if (this.wss) {\n this.wss.close(() => {\n this.logger.debug(\"WebSocket server closed (no new connections)\");\n });\n }\n\n // Close stdin to stop accepting new commands\n if (this.stdinInterface) {\n this.stdinInterface.close();\n this.stdinInterface = null;\n this.logger.debug(\"Stdin closed\");\n }\n\n // Initiate session manager shutdown (broadcasts notification, drains commands)\n const result = await this.sessionManager.initiateShutdown(timeoutMs);\n\n if (result.timedOut) {\n this.logger.warn(\"Shutdown timed out\", {\n timeoutMs,\n drained: result.drained,\n pending: this.sessionManager.getInFlightCount(),\n });\n } else {\n this.logger.info(\"All in-flight commands completed\", { count: result.drained });\n }\n\n // Close all remaining WebSocket connections\n if (this.wss) {\n const clients = [...this.wss.clients];\n this.logger.info(\"Closing WebSocket connections\", { count: clients.length });\n for (const ws of clients) {\n try {\n ws.close(1001, \"Server shutting down\");\n } catch {\n // Ignore close errors\n }\n }\n }\n\n // Dispose all sessions\n const disposeResult = this.sessionManager.disposeAllSessions();\n this.logger.info(\"Sessions disposed\", {\n disposed: disposeResult.disposed,\n failed: disposeResult.failed,\n });\n\n // Record uptime before final flush so buffered sinks include it\n const uptimeMs = Date.now() - this.serverStartTime;\n this.metrics.gauge(MetricNames.SESSION_LIFETIME_SECONDS, Math.floor(uptimeMs / 1000));\n\n // Flush metrics before shutdown\n await this.metrics.flush();\n\n this.logger.info(\"Shutdown complete\", { uptimeMs });\n }\n\n // ==========================================================================\n // WEBSOCKET TRANSPORT\n // ==========================================================================\n\n private setupWebSocket(wss: WebSocketServer): void {\n wss.on(\"connection\", async (ws: WebSocket, request: any) => {\n // Check connection limit\n const connResult = this.sessionManager.getGovernor().canAcceptConnection();\n if (!connResult.allowed) {\n this.logger.warn(\"Connection rejected\", { reason: connResult.reason });\n try {\n ws.close(1013, connResult.reason);\n } catch {\n // Ignore close errors\n }\n return;\n }\n\n // Authenticate connection\n const authContext: AuthContext = {\n request,\n websocket: {\n remoteAddress: request.socket?.remoteAddress,\n secure: request.socket?.encrypted ?? false,\n },\n serverStartTime: this.serverStartTime,\n connectionCount: this.sessionManager.getGovernor().getConnectionCount(),\n };\n\n const authResult = await Promise.resolve(this.authProvider.authenticate(authContext));\n if (!authResult.allowed) {\n this.logger.warn(\"Authentication failed\", { reason: authResult.reason });\n try {\n ws.close(1008, authResult.reason); // 1008 = Policy Violation\n } catch {\n // Ignore close errors\n }\n return;\n }\n\n // Register connection\n this.sessionManager.getGovernor().registerConnection();\n\n // Record connection metric\n this.metrics.counter(MetricNames.CONNECTIONS_TOTAL, 1);\n this.metrics.gauge(\n MetricNames.CONNECTIONS_ACTIVE,\n this.sessionManager.getGovernor().getConnectionCount()\n );\n\n // Initialize heartbeat state\n const heartbeatState: WebSocketConnectionState = {\n waitingForPong: false,\n lastPongAt: Date.now(),\n heartbeatTimer: null,\n pongTimeoutTimer: null,\n cleanedUp: false,\n };\n\n const subscriber: Subscriber = {\n send: (data: string) => {\n // Use backpressure-aware send for broadcast messages (non-critical)\n sendWithBackpressure(ws, data, { isCritical: false });\n },\n subscribedSessions: new Set(),\n };\n\n // Send server_ready to new connection (critical - must be delivered)\n const readyEvent: RpcBroadcast = {\n type: \"server_ready\",\n data: {\n serverVersion: SERVER_VERSION,\n protocolVersion: PROTOCOL_VERSION,\n transports: [\"websocket\", \"stdio\"],\n },\n };\n sendWithBackpressure(ws, JSON.stringify(readyEvent), { isCritical: true });\n\n this.sessionManager.addSubscriber(subscriber);\n\n let cleanedUp = false;\n const cleanupConnection = () => {\n if (cleanedUp) return;\n cleanedUp = true;\n stopHeartbeat(heartbeatState);\n this.sessionManager.removeSubscriber(subscriber);\n this.sessionManager.getGovernor().unregisterConnection();\n // Update connection metrics\n this.metrics.gauge(\n MetricNames.CONNECTIONS_ACTIVE,\n this.sessionManager.getGovernor().getConnectionCount()\n );\n };\n\n // Start heartbeat monitoring\n startHeartbeat(ws, heartbeatState);\n\n // Handle pong responses\n ws.on(\"pong\", () => {\n heartbeatState.waitingForPong = false;\n heartbeatState.lastPongAt = Date.now();\n if (heartbeatState.pongTimeoutTimer) {\n clearTimeout(heartbeatState.pongTimeoutTimer);\n heartbeatState.pongTimeoutTimer = null;\n }\n });\n\n ws.on(\"message\", async (data: Buffer) => {\n // Check message size limit\n const sizeResult = this.sessionManager.getGovernor().canAcceptMessage(data.length);\n if (!sizeResult.allowed) {\n const errorResponse: RpcResponse = {\n type: \"response\",\n command: \"unknown\",\n success: false,\n error: sizeResult.reason,\n };\n // Error responses are critical - client needs to know why their message was rejected\n sendWithBackpressure(ws, JSON.stringify(errorResponse), { isCritical: true });\n return;\n }\n\n try {\n const command: RpcCommand = JSON.parse(data.toString());\n await this.handleCommand(command, subscriber, (response: RpcResponse) => {\n // Command responses are critical - client is waiting for them\n sendWithBackpressure(ws, JSON.stringify(response), { isCritical: true });\n });\n } catch (error) {\n const errorResponse: RpcResponse = {\n type: \"response\",\n command: \"unknown\",\n success: false,\n error: error instanceof Error ? error.message : \"Invalid JSON\",\n };\n // Parse error responses are critical\n sendWithBackpressure(ws, JSON.stringify(errorResponse), { isCritical: true });\n }\n });\n\n ws.on(\"close\", () => {\n cleanupConnection();\n });\n\n ws.on(\"error\", (error) => {\n console.error(`[WebSocket] Connection error:`, error);\n cleanupConnection();\n });\n });\n }\n\n // ==========================================================================\n // STDIO TRANSPORT\n // ==========================================================================\n\n private setupStdio(): readline.Interface {\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n terminal: false,\n });\n\n const subscriber: Subscriber = {\n send: (data: string) => {\n // Use backpressure-aware write (non-critical - broadcasts can be dropped)\n if (!sendWithStdioBackpressure(data, this.stdioState, { isCritical: false })) {\n this.stdioState.droppedCount++;\n }\n },\n subscribedSessions: new Set(),\n };\n\n this.sessionManager.addSubscriber(subscriber);\n\n rl.on(\"line\", async (line: string) => {\n // Check message size limit (bytes, not UTF-16 code units)\n const messageBytes = Buffer.byteLength(line, \"utf8\");\n const sizeResult = this.sessionManager.getGovernor().canAcceptMessage(messageBytes);\n if (!sizeResult.allowed) {\n const errorResponse: RpcResponse = {\n type: \"response\",\n command: \"unknown\",\n success: false,\n error: sizeResult.reason,\n };\n // Error responses are critical\n sendWithStdioBackpressure(JSON.stringify(errorResponse), this.stdioState, {\n isCritical: true,\n });\n return;\n }\n\n try {\n const command: RpcCommand = JSON.parse(line);\n await this.handleCommand(command, subscriber, (response: RpcResponse) => {\n // Command responses are critical\n sendWithStdioBackpressure(JSON.stringify(response), this.stdioState, {\n isCritical: true,\n });\n });\n } catch (error) {\n const errorResponse: RpcResponse = {\n type: \"response\",\n command: \"unknown\",\n success: false,\n error: error instanceof Error ? error.message : \"Invalid JSON\",\n };\n sendWithStdioBackpressure(JSON.stringify(errorResponse), this.stdioState, {\n isCritical: true,\n });\n }\n });\n\n rl.on(\"close\", () => {\n this.sessionManager.removeSubscriber(subscriber);\n });\n\n return rl;\n }\n\n // ==========================================================================\n // COMMAND HANDLING\n // ==========================================================================\n\n private async handleCommand(\n command: RpcCommand,\n subscriber: Subscriber,\n respond: (response: RpcResponse) => void\n ): Promise<void> {\n // Execute command\n const response = await this.sessionManager.executeCommand(command);\n respond(response);\n\n // Handle subscription AFTER successful switch_session\n if (command.type === \"switch_session\" && response.success) {\n const sessionId = getSessionIdFromCmd(command);\n if (sessionId) {\n this.sessionManager.subscribeToSession(subscriber, sessionId);\n }\n }\n\n // Broadcast session lifecycle events\n if (command.type === \"create_session\" && isCreateSessionResponse(response)) {\n const broadcast: RpcBroadcast = {\n type: \"session_created\",\n data: {\n sessionId: response.data.sessionId,\n sessionInfo: response.data.sessionInfo,\n },\n };\n this.sessionManager.broadcast(JSON.stringify(broadcast));\n } else if (command.type === \"delete_session\" && response.success) {\n const broadcast: RpcBroadcast = {\n type: \"session_deleted\",\n data: { sessionId: getSessionIdFromCmd(command)! },\n };\n this.sessionManager.broadcast(JSON.stringify(broadcast));\n }\n }\n}\n\n// ============================================================================\n// MAIN\n// ============================================================================\n\nasync function main(): Promise<void> {\n // Parse port with validation\n const portEnv = process.env.PI_SERVER_PORT;\n const port = portEnv ? parseInt(portEnv, 10) : DEFAULT_PORT;\n\n if (Number.isNaN(port) || port < 1 || port > 65535) {\n console.error(`Invalid PI_SERVER_PORT: \"${portEnv}\". Must be 1-65535.`);\n process.exit(1);\n return; // TypeScript needs this\n }\n\n const server = new PiServer();\n await server.start(port);\n\n // Graceful shutdown handlers\n const handleShutdown = async (signal: string) => {\n console.error(`\\n[${signal}] Received, initiating shutdown...`);\n await server.stop(DEFAULT_SHUTDOWN_TIMEOUT_MS);\n process.exit(0);\n };\n\n process.on(\"SIGINT\", () => handleShutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => handleShutdown(\"SIGTERM\"));\n}\n\nif (import.meta.url === `file://${process.argv[1]}`) {\n main().catch((error) => {\n console.error(\"Fatal error:\", error);\n process.exit(1);\n });\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Session Lock Manager - provides mutual exclusion for session ID operations.
3
+ *
4
+ * Prevents race conditions in session creation/deletion:
5
+ * - Two concurrent createSession("same-id") calls both reserve slots
6
+ * - Only one succeeds, but both slots are consumed
7
+ *
8
+ * This manager provides per-session-ID locks with timeout to prevent deadlocks.
9
+ */
10
+ /**
11
+ * A lock handle that must be released after use.
12
+ * Implements RAII pattern via explicit release().
13
+ */
14
+ export interface SessionLockHandle {
15
+ sessionId: string;
16
+ acquiredAt: number;
17
+ release: () => void;
18
+ }
19
+ /**
20
+ * Configuration for the lock manager.
21
+ */
22
+ export interface SessionLockManagerOptions {
23
+ /** Timeout for lock acquisition (default: 5000ms). */
24
+ lockTimeoutMs?: number;
25
+ /** Maximum waiters per session lock (default: 100). Prevents memory exhaustion. */
26
+ maxQueueSize?: number;
27
+ /** Enable debug logging for lock operations. */
28
+ debug?: boolean;
29
+ }
30
+ /**
31
+ * Statistics about the lock manager state.
32
+ */
33
+ export interface SessionLockManagerStats {
34
+ /** Number of currently held locks. */
35
+ activeLocks: number;
36
+ /** Number of lock acquisitions that timed out. */
37
+ timeoutCount: number;
38
+ /** Number of currently waiting lock acquisitions. */
39
+ waitingCount: number;
40
+ /** Number of lock acquisitions rejected due to queue full. */
41
+ queueFullRejections: number;
42
+ }
43
+ /**
44
+ * Session Lock Manager - provides per-session-ID mutual exclusion.
45
+ *
46
+ * Usage:
47
+ * ```typescript
48
+ * const lockManager = new SessionLockManager();
49
+ *
50
+ * async function createSession(sessionId: string) {
51
+ * const lock = await lockManager.acquire(sessionId);
52
+ * try {
53
+ * // Critical section - only one caller per sessionId
54
+ * await doCreateSession(sessionId);
55
+ * } finally {
56
+ * lock.release();
57
+ * }
58
+ * }
59
+ * ```
60
+ */
61
+ export declare class SessionLockManager {
62
+ private locks;
63
+ private waitingQueues;
64
+ private timeoutCount;
65
+ private queueFullRejections;
66
+ private readonly lockTimeoutMs;
67
+ private readonly maxQueueSize;
68
+ private readonly debug;
69
+ constructor(options?: SessionLockManagerOptions);
70
+ /**
71
+ * Acquire a lock for a session ID.
72
+ * Returns a handle that must be released after use.
73
+ * Throws on timeout to prevent indefinite waiting.
74
+ */
75
+ acquire(sessionId: string, holder?: string): Promise<SessionLockHandle>;
76
+ /**
77
+ * Try to acquire a lock without waiting.
78
+ * Returns null if lock is held by another caller.
79
+ */
80
+ tryAcquire(sessionId: string, holder?: string): SessionLockHandle | null;
81
+ /**
82
+ * Check if a lock is currently held for a session ID.
83
+ */
84
+ isLocked(sessionId: string): boolean;
85
+ /**
86
+ * Get statistics about the lock manager.
87
+ */
88
+ getStats(): SessionLockManagerStats;
89
+ /**
90
+ * Clear all locks (used during disposal or testing).
91
+ * Warning: This can break invariants if locks are still held.
92
+ */
93
+ clear(): void;
94
+ private acquireImmediate;
95
+ private acquireImmediateSync;
96
+ private acquireQueued;
97
+ private release;
98
+ private log;
99
+ }
100
+ //# sourceMappingURL=session-lock-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-lock-manager.d.ts","sourceRoot":"","sources":["../src/session-lock-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAWH;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAYD;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,sDAAsD;IACtD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mFAAmF;IACnF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gDAAgD;IAChD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,sCAAsC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,YAAY,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,aAAa,CAGjB;IACJ,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,mBAAmB,CAAK;IAEhC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAU;gBAEpB,OAAO,GAAE,yBAA8B;IAYnD;;;;OAIG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAY7E;;;OAGG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI;IAOxE;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAIpC;;OAEG;IACH,QAAQ,IAAI,uBAAuB;IAcnC;;;OAGG;IACH,KAAK,IAAI,IAAI;IAkBb,OAAO,CAAC,gBAAgB;IAiCxB,OAAO,CAAC,oBAAoB;YAiCd,aAAa;IAoD3B,OAAO,CAAC,OAAO;IAwBf,OAAO,CAAC,GAAG;CAKZ"}
@@ -0,0 +1,199 @@
1
+ const DEFAULT_LOCK_TIMEOUT_MS = 5e3;
2
+ const LOCK_HOLD_WARNING_MS = 3e4;
3
+ const DEFAULT_MAX_QUEUE_SIZE = 100;
4
+ class SessionLockManager {
5
+ locks = /* @__PURE__ */ new Map();
6
+ waitingQueues = /* @__PURE__ */ new Map();
7
+ timeoutCount = 0;
8
+ queueFullRejections = 0;
9
+ lockTimeoutMs;
10
+ maxQueueSize;
11
+ debug;
12
+ constructor(options = {}) {
13
+ this.lockTimeoutMs = typeof options.lockTimeoutMs === "number" && options.lockTimeoutMs > 0 ? options.lockTimeoutMs : DEFAULT_LOCK_TIMEOUT_MS;
14
+ this.maxQueueSize = typeof options.maxQueueSize === "number" && options.maxQueueSize > 0 ? options.maxQueueSize : DEFAULT_MAX_QUEUE_SIZE;
15
+ this.debug = options.debug ?? false;
16
+ }
17
+ /**
18
+ * Acquire a lock for a session ID.
19
+ * Returns a handle that must be released after use.
20
+ * Throws on timeout to prevent indefinite waiting.
21
+ */
22
+ async acquire(sessionId, holder) {
23
+ const existing = this.locks.get(sessionId);
24
+ if (!existing) {
25
+ return this.acquireImmediate(sessionId, holder);
26
+ }
27
+ return this.acquireQueued(sessionId, holder);
28
+ }
29
+ /**
30
+ * Try to acquire a lock without waiting.
31
+ * Returns null if lock is held by another caller.
32
+ */
33
+ tryAcquire(sessionId, holder) {
34
+ if (this.locks.has(sessionId)) {
35
+ return null;
36
+ }
37
+ return this.acquireImmediateSync(sessionId, holder);
38
+ }
39
+ /**
40
+ * Check if a lock is currently held for a session ID.
41
+ */
42
+ isLocked(sessionId) {
43
+ return this.locks.has(sessionId);
44
+ }
45
+ /**
46
+ * Get statistics about the lock manager.
47
+ */
48
+ getStats() {
49
+ let waitingCount = 0;
50
+ for (const queue of this.waitingQueues.values()) {
51
+ waitingCount += queue.length;
52
+ }
53
+ return {
54
+ activeLocks: this.locks.size,
55
+ timeoutCount: this.timeoutCount,
56
+ waitingCount,
57
+ queueFullRejections: this.queueFullRejections
58
+ };
59
+ }
60
+ /**
61
+ * Clear all locks (used during disposal or testing).
62
+ * Warning: This can break invariants if locks are still held.
63
+ */
64
+ clear() {
65
+ for (const [sessionId, queue] of this.waitingQueues) {
66
+ for (const { reject } of queue) {
67
+ reject(new Error(`Lock manager cleared while waiting for ${sessionId}`));
68
+ }
69
+ }
70
+ this.waitingQueues.clear();
71
+ this.locks.clear();
72
+ this.timeoutCount = 0;
73
+ this.queueFullRejections = 0;
74
+ }
75
+ // ===========================================================================
76
+ // PRIVATE
77
+ // ===========================================================================
78
+ acquireImmediate(sessionId, holder) {
79
+ let resolveLock;
80
+ const promise = new Promise((resolve) => {
81
+ resolveLock = resolve;
82
+ });
83
+ const state = {
84
+ promise,
85
+ resolve: resolveLock,
86
+ acquiredAt: Date.now(),
87
+ holder
88
+ };
89
+ this.locks.set(sessionId, state);
90
+ this.log(`Acquired lock for ${sessionId}`, holder);
91
+ const warningTimer = setTimeout(() => {
92
+ console.warn(
93
+ `[SessionLockManager] Lock held for ${sessionId} > ${LOCK_HOLD_WARNING_MS}ms by ${holder ?? "unknown"}`
94
+ );
95
+ }, LOCK_HOLD_WARNING_MS);
96
+ return {
97
+ sessionId,
98
+ acquiredAt: state.acquiredAt,
99
+ release: () => {
100
+ clearTimeout(warningTimer);
101
+ this.release(sessionId, state);
102
+ }
103
+ };
104
+ }
105
+ acquireImmediateSync(sessionId, holder) {
106
+ let resolveLock;
107
+ const promise = new Promise((resolve) => {
108
+ resolveLock = resolve;
109
+ });
110
+ const state = {
111
+ promise,
112
+ resolve: resolveLock,
113
+ acquiredAt: Date.now(),
114
+ holder
115
+ };
116
+ this.locks.set(sessionId, state);
117
+ this.log(`Acquired lock (sync) for ${sessionId}`, holder);
118
+ const warningTimer = setTimeout(() => {
119
+ console.warn(
120
+ `[SessionLockManager] Lock held for ${sessionId} > ${LOCK_HOLD_WARNING_MS}ms by ${holder ?? "unknown"}`
121
+ );
122
+ }, LOCK_HOLD_WARNING_MS);
123
+ return {
124
+ sessionId,
125
+ acquiredAt: state.acquiredAt,
126
+ release: () => {
127
+ clearTimeout(warningTimer);
128
+ this.release(sessionId, state);
129
+ }
130
+ };
131
+ }
132
+ async acquireQueued(sessionId, holder) {
133
+ let queue = this.waitingQueues.get(sessionId);
134
+ if (!queue) {
135
+ queue = [];
136
+ this.waitingQueues.set(sessionId, queue);
137
+ }
138
+ if (queue.length >= this.maxQueueSize) {
139
+ this.queueFullRejections++;
140
+ throw new Error(
141
+ `Lock queue full for session ${sessionId} (max ${this.maxQueueSize} waiters)`
142
+ );
143
+ }
144
+ this.log(`Queued for lock on ${sessionId}`, holder);
145
+ return new Promise((resolve, reject) => {
146
+ const timeoutId = setTimeout(() => {
147
+ const idx = queue.indexOf(entry);
148
+ if (idx !== -1) {
149
+ queue.splice(idx, 1);
150
+ }
151
+ if (queue.length === 0) {
152
+ this.waitingQueues.delete(sessionId);
153
+ }
154
+ this.timeoutCount++;
155
+ this.log(`Timeout waiting for lock on ${sessionId}`, holder);
156
+ reject(
157
+ new Error(`Lock acquisition timeout for session ${sessionId} (${this.lockTimeoutMs}ms)`)
158
+ );
159
+ }, this.lockTimeoutMs);
160
+ const entry = {
161
+ resolve: (handle) => {
162
+ clearTimeout(timeoutId);
163
+ resolve(handle);
164
+ },
165
+ reject: (error) => {
166
+ clearTimeout(timeoutId);
167
+ reject(error);
168
+ }
169
+ };
170
+ queue.push(entry);
171
+ });
172
+ }
173
+ release(sessionId, state) {
174
+ if (this.locks.get(sessionId) !== state) {
175
+ this.log(`Ignoring stale release for ${sessionId}`);
176
+ return;
177
+ }
178
+ this.locks.delete(sessionId);
179
+ this.log(`Released lock for ${sessionId}`, state.holder);
180
+ const queue = this.waitingQueues.get(sessionId);
181
+ if (queue && queue.length > 0) {
182
+ const next = queue.shift();
183
+ if (queue.length === 0) {
184
+ this.waitingQueues.delete(sessionId);
185
+ }
186
+ const handle = this.acquireImmediateSync(sessionId, `from-queue:${state.holder}`);
187
+ next.resolve(handle);
188
+ }
189
+ }
190
+ log(message, context) {
191
+ if (this.debug) {
192
+ console.error(`[SessionLockManager] ${message}${context ? ` (${context})` : ""}`);
193
+ }
194
+ }
195
+ }
196
+ export {
197
+ SessionLockManager
198
+ };
199
+ //# sourceMappingURL=session-lock-manager.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/session-lock-manager.ts"],
4
+ "sourcesContent": ["/**\n * Session Lock Manager - provides mutual exclusion for session ID operations.\n *\n * Prevents race conditions in session creation/deletion:\n * - Two concurrent createSession(\"same-id\") calls both reserve slots\n * - Only one succeeds, but both slots are consumed\n *\n * This manager provides per-session-ID locks with timeout to prevent deadlocks.\n */\n\n/** Default timeout for lock acquisition (5 seconds). */\nconst DEFAULT_LOCK_TIMEOUT_MS = 5000;\n\n/** Maximum time to hold a lock before warning (30 seconds). */\nconst LOCK_HOLD_WARNING_MS = 30000;\n\n/** Maximum number of waiters per session lock (prevents memory exhaustion). */\nconst DEFAULT_MAX_QUEUE_SIZE = 100;\n\n/**\n * A lock handle that must be released after use.\n * Implements RAII pattern via explicit release().\n */\nexport interface SessionLockHandle {\n sessionId: string;\n acquiredAt: number;\n release: () => void;\n}\n\n/**\n * Internal lock state.\n */\ninterface LockState {\n promise: Promise<void>;\n resolve: () => void;\n acquiredAt: number;\n holder?: string; // Debug info about who holds the lock\n}\n\n/**\n * Configuration for the lock manager.\n */\nexport interface SessionLockManagerOptions {\n /** Timeout for lock acquisition (default: 5000ms). */\n lockTimeoutMs?: number;\n /** Maximum waiters per session lock (default: 100). Prevents memory exhaustion. */\n maxQueueSize?: number;\n /** Enable debug logging for lock operations. */\n debug?: boolean;\n}\n\n/**\n * Statistics about the lock manager state.\n */\nexport interface SessionLockManagerStats {\n /** Number of currently held locks. */\n activeLocks: number;\n /** Number of lock acquisitions that timed out. */\n timeoutCount: number;\n /** Number of currently waiting lock acquisitions. */\n waitingCount: number;\n /** Number of lock acquisitions rejected due to queue full. */\n queueFullRejections: number;\n}\n\n/**\n * Session Lock Manager - provides per-session-ID mutual exclusion.\n *\n * Usage:\n * ```typescript\n * const lockManager = new SessionLockManager();\n *\n * async function createSession(sessionId: string) {\n * const lock = await lockManager.acquire(sessionId);\n * try {\n * // Critical section - only one caller per sessionId\n * await doCreateSession(sessionId);\n * } finally {\n * lock.release();\n * }\n * }\n * ```\n */\nexport class SessionLockManager {\n private locks = new Map<string, LockState>();\n private waitingQueues = new Map<\n string,\n Array<{ resolve: (handle: SessionLockHandle) => void; reject: (error: Error) => void }>\n >();\n private timeoutCount = 0;\n private queueFullRejections = 0;\n\n private readonly lockTimeoutMs: number;\n private readonly maxQueueSize: number;\n private readonly debug: boolean;\n\n constructor(options: SessionLockManagerOptions = {}) {\n this.lockTimeoutMs =\n typeof options.lockTimeoutMs === \"number\" && options.lockTimeoutMs > 0\n ? options.lockTimeoutMs\n : DEFAULT_LOCK_TIMEOUT_MS;\n this.maxQueueSize =\n typeof options.maxQueueSize === \"number\" && options.maxQueueSize > 0\n ? options.maxQueueSize\n : DEFAULT_MAX_QUEUE_SIZE;\n this.debug = options.debug ?? false;\n }\n\n /**\n * Acquire a lock for a session ID.\n * Returns a handle that must be released after use.\n * Throws on timeout to prevent indefinite waiting.\n */\n async acquire(sessionId: string, holder?: string): Promise<SessionLockHandle> {\n const existing = this.locks.get(sessionId);\n\n if (!existing) {\n // Lock is free, acquire immediately\n return this.acquireImmediate(sessionId, holder);\n }\n\n // Lock is held, wait in queue\n return this.acquireQueued(sessionId, holder);\n }\n\n /**\n * Try to acquire a lock without waiting.\n * Returns null if lock is held by another caller.\n */\n tryAcquire(sessionId: string, holder?: string): SessionLockHandle | null {\n if (this.locks.has(sessionId)) {\n return null;\n }\n return this.acquireImmediateSync(sessionId, holder);\n }\n\n /**\n * Check if a lock is currently held for a session ID.\n */\n isLocked(sessionId: string): boolean {\n return this.locks.has(sessionId);\n }\n\n /**\n * Get statistics about the lock manager.\n */\n getStats(): SessionLockManagerStats {\n let waitingCount = 0;\n for (const queue of this.waitingQueues.values()) {\n waitingCount += queue.length;\n }\n\n return {\n activeLocks: this.locks.size,\n timeoutCount: this.timeoutCount,\n waitingCount,\n queueFullRejections: this.queueFullRejections,\n };\n }\n\n /**\n * Clear all locks (used during disposal or testing).\n * Warning: This can break invariants if locks are still held.\n */\n clear(): void {\n // Reject all waiting acquires\n for (const [sessionId, queue] of this.waitingQueues) {\n for (const { reject } of queue) {\n reject(new Error(`Lock manager cleared while waiting for ${sessionId}`));\n }\n }\n\n this.waitingQueues.clear();\n this.locks.clear();\n this.timeoutCount = 0;\n this.queueFullRejections = 0;\n }\n\n // ===========================================================================\n // PRIVATE\n // ===========================================================================\n\n private acquireImmediate(sessionId: string, holder?: string): SessionLockHandle {\n let resolveLock: () => void;\n const promise = new Promise<void>((resolve) => {\n resolveLock = resolve;\n });\n\n const state: LockState = {\n promise,\n resolve: resolveLock!,\n acquiredAt: Date.now(),\n holder,\n };\n\n this.locks.set(sessionId, state);\n this.log(`Acquired lock for ${sessionId}`, holder);\n\n // Set up long-hold warning\n const warningTimer = setTimeout(() => {\n console.warn(\n `[SessionLockManager] Lock held for ${sessionId} > ${LOCK_HOLD_WARNING_MS}ms by ${holder ?? \"unknown\"}`\n );\n }, LOCK_HOLD_WARNING_MS);\n\n return {\n sessionId,\n acquiredAt: state.acquiredAt,\n release: () => {\n clearTimeout(warningTimer);\n this.release(sessionId, state);\n },\n };\n }\n\n private acquireImmediateSync(sessionId: string, holder?: string): SessionLockHandle {\n // Same as acquireImmediate but synchronous (no promise creation overhead)\n let resolveLock: () => void;\n const promise = new Promise<void>((resolve) => {\n resolveLock = resolve;\n });\n\n const state: LockState = {\n promise,\n resolve: resolveLock!,\n acquiredAt: Date.now(),\n holder,\n };\n\n this.locks.set(sessionId, state);\n this.log(`Acquired lock (sync) for ${sessionId}`, holder);\n\n const warningTimer = setTimeout(() => {\n console.warn(\n `[SessionLockManager] Lock held for ${sessionId} > ${LOCK_HOLD_WARNING_MS}ms by ${holder ?? \"unknown\"}`\n );\n }, LOCK_HOLD_WARNING_MS);\n\n return {\n sessionId,\n acquiredAt: state.acquiredAt,\n release: () => {\n clearTimeout(warningTimer);\n this.release(sessionId, state);\n },\n };\n }\n\n private async acquireQueued(sessionId: string, holder?: string): Promise<SessionLockHandle> {\n // Get or create queue for this session\n let queue = this.waitingQueues.get(sessionId);\n if (!queue) {\n queue = [];\n this.waitingQueues.set(sessionId, queue);\n }\n\n // Check queue size limit to prevent memory exhaustion\n if (queue.length >= this.maxQueueSize) {\n this.queueFullRejections++;\n throw new Error(\n `Lock queue full for session ${sessionId} (max ${this.maxQueueSize} waiters)`\n );\n }\n\n this.log(`Queued for lock on ${sessionId}`, holder);\n\n return new Promise<SessionLockHandle>((resolve, reject) => {\n // Set up timeout\n const timeoutId = setTimeout(() => {\n // Remove from queue\n const idx = queue!.indexOf(entry);\n if (idx !== -1) {\n queue!.splice(idx, 1);\n }\n if (queue!.length === 0) {\n this.waitingQueues.delete(sessionId);\n }\n\n this.timeoutCount++;\n this.log(`Timeout waiting for lock on ${sessionId}`, holder);\n reject(\n new Error(`Lock acquisition timeout for session ${sessionId} (${this.lockTimeoutMs}ms)`)\n );\n }, this.lockTimeoutMs);\n\n const entry = {\n resolve: (handle: SessionLockHandle) => {\n clearTimeout(timeoutId);\n resolve(handle);\n },\n reject: (error: Error) => {\n clearTimeout(timeoutId);\n reject(error);\n },\n };\n\n queue!.push(entry);\n });\n }\n\n private release(sessionId: string, state: LockState): void {\n // Only release if this is the current lock holder (prevents double-release)\n if (this.locks.get(sessionId) !== state) {\n this.log(`Ignoring stale release for ${sessionId}`);\n return;\n }\n\n this.locks.delete(sessionId);\n this.log(`Released lock for ${sessionId}`, state.holder);\n\n // Wake up next waiter if any\n const queue = this.waitingQueues.get(sessionId);\n if (queue && queue.length > 0) {\n const next = queue.shift()!;\n if (queue.length === 0) {\n this.waitingQueues.delete(sessionId);\n }\n\n // Give the lock to the next waiter\n const handle = this.acquireImmediateSync(sessionId, `from-queue:${state.holder}`);\n next.resolve(handle);\n }\n }\n\n private log(message: string, context?: string): void {\n if (this.debug) {\n console.error(`[SessionLockManager] ${message}${context ? ` (${context})` : \"\"}`);\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Session Manager - owns session lifecycle, command execution, and subscriber maps.
3
+ *
4
+ * RESPONSIBILITIES (per AGENTS.md):
5
+ * - Orchestration: coordinates stores, engines, sessions
6
+ * - Session lifecycle (create, delete, list, load)
7
+ * - Subscriber and event broadcast management
8
+ * - Command execution pipeline (tracking, rate limiting, replay)
9
+ *
10
+ * DOES NOT:
11
+ * - Handle server commands directly (delegates to server-command-handlers.ts)
12
+ * - Handle session commands directly (delegates to command-router.ts)
13
+ * - Mutate state directly (delegates to stores)
14
+ */
15
+ import { type AgentSession } from "@mariozechner/pi-coding-agent";
16
+ import type { RpcCommand, RpcResponse, SessionInfo, SessionResolver, Subscriber } from "./types.js";
17
+ import { ResourceGovernor } from "./resource-governor.js";
18
+ import { SessionStore, type StoredSessionInfo } from "./session-store.js";
19
+ import { CircuitBreakerManager, type CircuitBreakerConfig } from "./circuit-breaker.js";
20
+ import { BashCircuitBreaker, type BashCircuitBreakerConfig } from "./bash-circuit-breaker.js";
21
+ export interface SessionManagerRuntimeOptions {
22
+ defaultCommandTimeoutMs?: number;
23
+ shortCommandTimeoutMs?: number;
24
+ dependencyWaitTimeoutMs?: number;
25
+ idempotencyTtlMs?: number;
26
+ /** Server version for session metadata tracking */
27
+ serverVersion?: string;
28
+ /** Circuit breaker configuration (optional, uses defaults if not provided) */
29
+ circuitBreakerConfig?: Partial<Omit<CircuitBreakerConfig, "providerName">>;
30
+ /** Bash circuit breaker configuration (optional, uses defaults if not provided) */
31
+ bashCircuitBreakerConfig?: Partial<BashCircuitBreakerConfig>;
32
+ }
33
+ export declare class PiSessionManager implements SessionResolver {
34
+ private sessions;
35
+ private sessionCreatedAt;
36
+ private subscribers;
37
+ private unsubscribers;
38
+ private governor;
39
+ /** Command replay and idempotency store. */
40
+ private replayStore;
41
+ /** Session version store. */
42
+ private versionStore;
43
+ /** Command execution engine. */
44
+ private executionEngine;
45
+ /** Session ID lock manager for preventing create/delete races. */
46
+ private lockManager;
47
+ /** Session metadata store for persistence across restarts (ADR-0007). */
48
+ private sessionStore;
49
+ /** Circuit breaker for LLM providers (ADR-0010). */
50
+ private circuitBreakers;
51
+ /** Circuit breaker for bash commands. */
52
+ private bashCircuitBreaker;
53
+ private isShuttingDown;
54
+ private inFlightCommands;
55
+ private sessionExpirationTimer;
56
+ private readonly defaultCommandTimeoutMs;
57
+ private readonly shortCommandTimeoutMs;
58
+ private readonly dependencyWaitTimeoutMs;
59
+ private extensionUI;
60
+ /** Optional memory metrics provider (set by server for ADR-0016) */
61
+ private memoryMetricsProvider;
62
+ constructor(governor?: ResourceGovernor, options?: SessionManagerRuntimeOptions);
63
+ /**
64
+ * Get the resource governor for external checks (e.g., message size).
65
+ */
66
+ getGovernor(): ResourceGovernor;
67
+ /**
68
+ * Get the circuit breaker manager for external access (e.g., admin operations).
69
+ */
70
+ getCircuitBreakers(): CircuitBreakerManager;
71
+ /**
72
+ * Get the bash circuit breaker for external access.
73
+ */
74
+ getBashCircuitBreaker(): BashCircuitBreaker;
75
+ /**
76
+ * Check if the server is shutting down.
77
+ */
78
+ isInShutdown(): boolean;
79
+ /**
80
+ * Set the memory metrics provider for ADR-0016 metrics system.
81
+ * Called by PiServer to provide access to MemorySink metrics.
82
+ */
83
+ setMemoryMetricsProvider(provider: () => Record<string, unknown> | undefined): void;
84
+ /**
85
+ * Initiate graceful shutdown.
86
+ * - Stops accepting new commands
87
+ * - Broadcasts shutdown notification to all clients
88
+ * - Returns promise that resolves when all in-flight commands complete or timeout
89
+ *
90
+ * Idempotent: calling multiple times returns the same result.
91
+ */
92
+ initiateShutdown(timeoutMs?: number): Promise<{
93
+ drained: number;
94
+ timedOut: boolean;
95
+ }>;
96
+ /**
97
+ * Dispose all sessions. Call after shutdown drain completes.
98
+ */
99
+ disposeAllSessions(): {
100
+ disposed: number;
101
+ failed: number;
102
+ };
103
+ /**
104
+ * Get count of in-flight commands.
105
+ */
106
+ getInFlightCount(): number;
107
+ /**
108
+ * Register an in-flight command promise for shutdown draining.
109
+ */
110
+ private registerInFlightCommand;
111
+ private broadcastCommandLifecycle;
112
+ createSession(sessionId: string, cwd?: string): Promise<SessionInfo>;
113
+ deleteSession(sessionId: string): Promise<void>;
114
+ getSession(sessionId: string): AgentSession | undefined;
115
+ getSessionInfo(sessionId: string): SessionInfo | undefined;
116
+ listSessions(): SessionInfo[];
117
+ /**
118
+ * List stored sessions that can be loaded.
119
+ * These are sessions that existed in previous server runs OR discovered on disk.
120
+ */
121
+ listStoredSessions(): Promise<StoredSessionInfo[]>;
122
+ /**
123
+ * Load a session from a stored session file.
124
+ * Creates a new in-memory session that reads from the existing session file.
125
+ */
126
+ loadSession(sessionId: string, sessionPath: string): Promise<SessionInfo>;
127
+ /**
128
+ * Get the session store for direct access (e.g., cleanup).
129
+ */
130
+ getSessionStore(): SessionStore;
131
+ /**
132
+ * Start periodic cleanup of orphaned session metadata and expired sessions.
133
+ * @param intervalMs Cleanup interval in milliseconds (default: 1 hour)
134
+ */
135
+ startSessionCleanup(intervalMs?: number): void;
136
+ /**
137
+ * Stop periodic cleanup.
138
+ */
139
+ stopSessionCleanup(): void;
140
+ /**
141
+ * Run a one-time cleanup of orphaned session metadata and expired sessions.
142
+ */
143
+ cleanupSessions(): Promise<{
144
+ removed: number;
145
+ kept: number;
146
+ }>;
147
+ /**
148
+ * Start periodic check for expired sessions (maxSessionLifetimeMs).
149
+ */
150
+ private startSessionExpirationCheck;
151
+ /**
152
+ * Stop periodic session expiration check.
153
+ */
154
+ private stopSessionExpirationCheck;
155
+ /**
156
+ * Clean up sessions that have exceeded maxSessionLifetimeMs.
157
+ * Also cleans up stale circuit breakers for unused providers.
158
+ */
159
+ private cleanupExpiredSessions;
160
+ addSubscriber(subscriber: Subscriber): void;
161
+ removeSubscriber(subscriber: Subscriber): void;
162
+ subscribeToSession(subscriber: Subscriber, sessionId: string): void;
163
+ unsubscribeFromSession(subscriber: Subscriber, sessionId: string): void;
164
+ private broadcastEvent;
165
+ broadcast(data: string): void;
166
+ /**
167
+ * Create the command execution context for server command handlers.
168
+ * This is the NEXUS seam - provides everything handlers need without
169
+ * direct coupling to SessionManager internals.
170
+ */
171
+ private createCommandContext;
172
+ /**
173
+ * Build the metrics response (extracted for handler use).
174
+ */
175
+ private buildMetricsResponse;
176
+ /**
177
+ * Build the health check response (extracted for handler use).
178
+ */
179
+ private buildHealthResponse;
180
+ executeCommand(command: RpcCommand): Promise<RpcResponse>;
181
+ /**
182
+ * Internal command execution (called after tracking and rate limiting).
183
+ * Routes to server command handlers or session command handlers.
184
+ */
185
+ private executeCommandInternal;
186
+ /**
187
+ * Create an AgentSession while sanitizing npm prefix env leakage from npm scripts.
188
+ *
189
+ * npm sets npm_config_prefix for child processes. If inherited here,
190
+ * pi-coding-agent's global package installation can be redirected into the
191
+ * current project (e.g. ./lib/node_modules), causing flaky session creation.
192
+ */
193
+ private createAgentSessionWithSanitizedNpmEnv;
194
+ private generateSessionId;
195
+ }
196
+ //# sourceMappingURL=session-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"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"}