Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brave-pillows-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cephalization/phoenix-insight": minor
---

fix: Forcefully shutdown keep-alive connections on close
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,26 @@ phoenix-insight/
└── README.md # This file
```

## Requirements

- **Node.js v22 or newer** - Required for the CLI to run
- **Anthropic API key** - Required for the AI agent

Set your Anthropic API key before running:

```bash
export ANTHROPIC_API_KEY=sk-ant-api03-...
```

You can get an API key from [console.anthropic.com](https://console.anthropic.com/).

## Development

### Prerequisites

- Node.js >= 18 (v24 recommended, see `.nvmrc`)
- Node.js v22 or newer
- pnpm 9.15.0 (`corepack enable && corepack prepare pnpm@9.15.0 --activate`)
- Anthropic API key (see [Requirements](#requirements))

### Setup

Expand Down
19 changes: 19 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ Phoenix Insight brings AI-powered analysis to your [Phoenix](https://github.com/

This filesystem-native approach provides transparency that traditional APIs can't match. Every query the agent runs is visible and reproducible. You can inspect the exact files it reads, copy its commands, and run them yourself. The data is just files, and the analysis is just bash, making AI-driven observability debuggable, auditable, and extensible with any tool in your Unix toolkit.

## Requirements

- **Node.js v22 or newer** - Required for the CLI to run
- **Anthropic API key** - Required for the AI agent

Set your Anthropic API key before running:

```bash
export ANTHROPIC_API_KEY=sk-ant-api03-...
```

Or add it to your shell profile for persistence:

```bash
echo 'export ANTHROPIC_API_KEY=sk-ant-api03-...' >> ~/.zshrc # or ~/.bashrc
```

You can get an API key from [console.anthropic.com](https://console.anthropic.com/).

## Installation

```bash
Expand Down
47 changes: 46 additions & 1 deletion packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,30 @@ function formatBashCommand(command: string): string {
}
}

/**
* Check if ANTHROPIC_API_KEY is set and provide a helpful error if not
*/
function ensureAnthropicApiKey(): void {
if (!process.env.ANTHROPIC_API_KEY) {
console.error(
"\n❌ Error: Missing ANTHROPIC_API_KEY environment variable\n"
);
console.error("The Anthropic API key is required to run the AI agent.\n");
console.error("To fix this, set the environment variable:\n");
console.error(" export ANTHROPIC_API_KEY=sk-ant-api03-...\n");
console.error(
"Or add it to your shell profile (~/.zshrc, ~/.bashrc, etc.):\n"
);
console.error(
" echo 'export ANTHROPIC_API_KEY=sk-ant-api03-...' >> ~/.zshrc\n"
);
console.error(
"You can get an API key from: https://console.anthropic.com/\n"
);
process.exit(1);
}
}

/**
* Handle errors with appropriate exit codes and user-friendly messages
*/
Expand Down Expand Up @@ -448,6 +472,9 @@ program
return;
}

// Ensure Anthropic API key is available for agent execution
ensureAnthropicApiKey();

// Initialize observability if trace is enabled in config
if (config.trace) {
initializeObservability({
Expand Down Expand Up @@ -622,6 +649,9 @@ async function runUIServer(options: {
port?: number;
open?: boolean;
}): Promise<void> {
// Ensure Anthropic API key is available for agent execution
ensureAnthropicApiKey();

const config = getConfig();
const port = options.port ?? 6007;
const shouldOpen = options.open !== false; // Default to opening browser
Expand Down Expand Up @@ -735,14 +765,24 @@ async function runUIServer(options: {
openBrowser(url);
}

// Handle graceful shutdown
// Handle graceful shutdown with timeout
let isShuttingDown = false;
const SHUTDOWN_TIMEOUT_MS = 3000;

const shutdown = async (signal: string) => {
if (isShuttingDown) return;
isShuttingDown = true;

console.log(`\n\n📥 Received ${signal}, shutting down gracefully...`);

// Set up a timeout to force exit if graceful shutdown takes too long
const forceExitTimeout = setTimeout(() => {
console.log("⏱️ Shutdown timeout reached, forcing exit...");
wsServer.forceClose();
uiServer.forceClose();
process.exit(0);
}, SHUTDOWN_TIMEOUT_MS);

try {
// Close WebSocket connections first
await wsServer.close();
Expand All @@ -759,9 +799,11 @@ async function runUIServer(options: {
// Shutdown observability if enabled
await shutdownObservability();

clearTimeout(forceExitTimeout);
console.log("👋 Server stopped. Goodbye!");
process.exit(0);
} catch (error) {
clearTimeout(forceExitTimeout);
console.error("Error during shutdown:", error);
process.exit(1);
}
Expand All @@ -780,6 +822,9 @@ async function runUIServer(options: {
}

async function runInteractiveMode(): Promise<void> {
// Ensure Anthropic API key is available for agent execution
ensureAnthropicApiKey();

const config = getConfig();

console.log("🚀 Phoenix Insight Interactive Mode");
Expand Down
22 changes: 21 additions & 1 deletion packages/cli/src/server/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,10 @@ export interface UIServer {
host: string;
/** Path to the UI dist directory being served */
distPath: string;
/** Close the server */
/** Close the server gracefully */
close(): Promise<void>;
/** Force close all connections immediately */
forceClose(): void;
}

// ============================================================================
Expand Down Expand Up @@ -189,6 +191,9 @@ export function createUIServer(options: UIServerOptions = {}): Promise<UIServer>
}

return new Promise((resolve, reject) => {
// Track active connections for force-close capability
const activeConnections = new Set<import("node:net").Socket>();

const httpServer = createServer((req, res) => {
const urlPath = req.url ?? "/";

Expand Down Expand Up @@ -258,6 +263,14 @@ export function createUIServer(options: UIServerOptions = {}): Promise<UIServer>
});
});

// Track connections to enable force-close
httpServer.on("connection", (socket) => {
activeConnections.add(socket);
socket.on("close", () => {
activeConnections.delete(socket);
});
});

// Handle server errors
httpServer.on("error", (err) => {
reject(err);
Expand Down Expand Up @@ -287,6 +300,13 @@ export function createUIServer(options: UIServerOptions = {}): Promise<UIServer>
});
});
},
forceClose(): void {
// Destroy all active connections immediately
for (const socket of activeConnections) {
socket.destroy();
}
activeConnections.clear();
},
});
});
});
Expand Down
16 changes: 16 additions & 0 deletions packages/cli/src/server/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,22 @@ export class PhoenixWebSocketServer {
});
});
}

/**
* Force terminate all WebSocket connections immediately.
* Use this when graceful close doesn't complete in time.
*/
forceClose(): void {
if (!this.wss) {
return;
}

// Terminate all client connections immediately (no close handshake)
for (const client of this.clients) {
client.terminate();
}
this.clients.clear();
}
}

// ============================================================================
Expand Down