-
Notifications
You must be signed in to change notification settings - Fork 109
Description
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:
- Run
pnpm build && pnpm start - When the security warning appears, select "No, exit"
- Observe the process hangs (cursor disappears, no prompt return)
- 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)
- Priority 10: VSCode Server - stop accepting new connections
- Priority 20: MCP Client - disconnect from MCP servers
- Priority 30: LSP Manager - stop language servers
- Priority 40: Health Monitor - clear intervals
- Priority 50: Logger - flush pending logs and close streams
- 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
- Just add
process.exit(0)everywhere - Quick fix but doesn't properly clean up resources - 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.tsxto use ShutdownManager for exit - Add tests for graceful shutdown
- Update
/exitcommand to use ShutdownManager
Additional Context
- Immediate workaround: Add
process.exit(0)afterexit()inApp.tsxfor 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