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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions