Skip to content

feat: Implement graceful shutdown system #239

@JimStenstrom

Description

@JimStenstrom

Use Case

Selecting "No, exit" on the security warning banner causes the process to hang indefinitely instead of exiting. This reveals a broader architectural gap: nanocoder lacks a centralized graceful shutdown system.

Steps to Reproduce:

  1. Run pnpm build && pnpm start
  2. When the security warning appears, select "No, exit"
  3. Observe the process hangs (cursor disappears, no prompt return)
  4. Verify with ps aux | grep "node dist/cli" - process is still running

Root Cause Analysis

1. ink's exit() doesn't terminate the process

The handleExit function in App.tsx calls exit() from ink's useApp() hook, which only unmounts the React component tree. If there are active event listeners or open handles, the Node.js event loop stays alive.

2. Logger signal handlers don't exit

In source/utils/logging/index.ts, SIGTERM/SIGINT handlers flush logs but never call process.exit().

3. No centralized shutdown coordinator

Each service has its own cleanup method, but there's no orchestration:

Resource Location Cleanup Method Called on Exit?
Logger loggerProvider flush()end() No
LSP Manager lspManagerInstance shutdown() No
MCP Client via ToolManager disconnectMCP() No
VSCode Server serverInstance stop() No
Health Monitor HealthMonitor stop() No

Proposed Solution

Create a ShutdownManager

source/utils/shutdown/
├── index.ts              # Main export
├── shutdown-manager.ts   # Coordinator singleton
└── types.ts              # Interfaces
interface CleanupHandler {
    name: string;
    priority: number;  // Lower = runs first
    cleanup: () => Promise<void>;
}

class ShutdownManager {
    private handlers: CleanupHandler[] = [];
    private isShuttingDown = false;

    register(handler: CleanupHandler): void;
    unregister(name: string): void;
    async gracefulShutdown(exitCode?: number): Promise<never>;
}

Cleanup Order (by priority)

  1. Priority 10: VSCode Server - stop accepting new connections
  2. Priority 20: MCP Client - disconnect from MCP servers
  3. Priority 30: LSP Manager - stop language servers
  4. Priority 40: Health Monitor - clear intervals
  5. Priority 50: Logger - flush pending logs and close streams
  6. Priority 100: Call process.exit()

Integration Points

Services register their cleanup on init, signal handlers are centralized in ShutdownManager, and App.tsx uses shutdownManager.gracefulShutdown() for exit.

Alternatives Considered

  1. Just add process.exit(0) everywhere - Quick fix but doesn't properly clean up resources
  2. Use ink's render instance with waitUntilExit() - Doesn't solve the cleanup coordination problem

Tasks

  • Create source/utils/shutdown/ module with ShutdownManager
  • Register logger cleanup with ShutdownManager
  • Register LSP Manager cleanup with ShutdownManager
  • Register MCP Client cleanup with ShutdownManager
  • Register VSCode Server cleanup with ShutdownManager
  • Register Health Monitor cleanup with ShutdownManager
  • Update App.tsx to use ShutdownManager for exit
  • Add tests for graceful shutdown
  • Update /exit command to use ShutdownManager

Additional Context

  • Immediate workaround: Add process.exit(0) after exit() in App.tsx for the security banner (acceptable since no services need cleanup at that stage)
  • Related: Duplicate signal handler registration - logging module's handlers fire twice, suggesting module double-loading (separate investigation needed)

Environment

  • OS: macOS (Darwin 25.2.0)
  • Node.js: v24.3.0
  • nanocoder: v1.19.2

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions