feat: enhance /api/health with version, uptime, workerPath, and AI status
Replace hardcoded TEST-008 build ID with real package version. Add worker filesystem path, uptime counter, and AI provider status (including last interaction success/failure tracking) to the health endpoint response. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
+249
-256
File diff suppressed because one or more lines are too long
@@ -30,6 +30,19 @@ export interface RouteHandler {
|
|||||||
setupRoutes(app: Application): void;
|
setupRoutes(app: Application): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI provider status for health endpoint
|
||||||
|
*/
|
||||||
|
export interface AiStatus {
|
||||||
|
provider: string;
|
||||||
|
authMethod: string;
|
||||||
|
lastInteraction: {
|
||||||
|
timestamp: number;
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for initializing the server
|
* Options for initializing the server
|
||||||
*/
|
*/
|
||||||
@@ -42,6 +55,10 @@ export interface ServerOptions {
|
|||||||
onShutdown: () => Promise<void>;
|
onShutdown: () => Promise<void>;
|
||||||
/** Restart function for admin endpoints */
|
/** Restart function for admin endpoints */
|
||||||
onRestart: () => Promise<void>;
|
onRestart: () => Promise<void>;
|
||||||
|
/** Filesystem path to the worker entry point */
|
||||||
|
workerPath: string;
|
||||||
|
/** Callback to get current AI provider status */
|
||||||
|
getAiStatus: () => AiStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -140,20 +157,20 @@ export class Server {
|
|||||||
* Setup core system routes (health, readiness, version, admin)
|
* Setup core system routes (health, readiness, version, admin)
|
||||||
*/
|
*/
|
||||||
private setupCoreRoutes(): void {
|
private setupCoreRoutes(): void {
|
||||||
// Test build ID for debugging which build is running
|
|
||||||
const TEST_BUILD_ID = 'TEST-008-wrapper-ipc';
|
|
||||||
|
|
||||||
// Health check endpoint - always responds, even during initialization
|
// Health check endpoint - always responds, even during initialization
|
||||||
this.app.get('/api/health', (_req: Request, res: Response) => {
|
this.app.get('/api/health', (_req: Request, res: Response) => {
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
build: TEST_BUILD_ID,
|
version: BUILT_IN_VERSION,
|
||||||
|
workerPath: this.options.workerPath,
|
||||||
|
uptime: Date.now() - this.startTime,
|
||||||
managed: process.env.CLAUDE_MEM_MANAGED === 'true',
|
managed: process.env.CLAUDE_MEM_MANAGED === 'true',
|
||||||
hasIpc: typeof process.send === 'function',
|
hasIpc: typeof process.send === 'function',
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
initialized: this.options.getInitializationComplete(),
|
initialized: this.options.getInitializationComplete(),
|
||||||
mcpReady: this.options.getMcpReady(),
|
mcpReady: this.options.getMcpReady(),
|
||||||
|
ai: this.options.getAiStatus(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|||||||
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
||||||
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
|
||||||
import { SettingsDefaultsManager } from '../shared/SettingsDefaultsManager.js';
|
import { SettingsDefaultsManager } from '../shared/SettingsDefaultsManager.js';
|
||||||
|
import { getAuthMethodDescription } from '../shared/EnvManager.js';
|
||||||
import { logger } from '../utils/logger.js';
|
import { logger } from '../utils/logger.js';
|
||||||
|
|
||||||
// Windows: avoid repeated spawn popups when startup fails (issue #921)
|
// Windows: avoid repeated spawn popups when startup fails (issue #921)
|
||||||
@@ -171,6 +172,14 @@ export class WorkerService {
|
|||||||
// Orphan reaper cleanup function (Issue #737)
|
// Orphan reaper cleanup function (Issue #737)
|
||||||
private stopOrphanReaper: (() => void) | null = null;
|
private stopOrphanReaper: (() => void) | null = null;
|
||||||
|
|
||||||
|
// AI interaction tracking for health endpoint
|
||||||
|
private lastAiInteraction: {
|
||||||
|
timestamp: number;
|
||||||
|
success: boolean;
|
||||||
|
provider: string;
|
||||||
|
error?: string;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Initialize the promise that will resolve when background initialization completes
|
// Initialize the promise that will resolve when background initialization completes
|
||||||
this.initializationComplete = new Promise((resolve) => {
|
this.initializationComplete = new Promise((resolve) => {
|
||||||
@@ -207,7 +216,24 @@ export class WorkerService {
|
|||||||
getInitializationComplete: () => this.initializationCompleteFlag,
|
getInitializationComplete: () => this.initializationCompleteFlag,
|
||||||
getMcpReady: () => this.mcpReady,
|
getMcpReady: () => this.mcpReady,
|
||||||
onShutdown: () => this.shutdown(),
|
onShutdown: () => this.shutdown(),
|
||||||
onRestart: () => this.shutdown()
|
onRestart: () => this.shutdown(),
|
||||||
|
workerPath: __filename,
|
||||||
|
getAiStatus: () => {
|
||||||
|
let provider = 'claude';
|
||||||
|
if (isOpenRouterSelected() && isOpenRouterAvailable()) provider = 'openrouter';
|
||||||
|
else if (isGeminiSelected() && isGeminiAvailable()) provider = 'gemini';
|
||||||
|
return {
|
||||||
|
provider,
|
||||||
|
authMethod: getAuthMethodDescription(),
|
||||||
|
lastInteraction: this.lastAiInteraction
|
||||||
|
? {
|
||||||
|
timestamp: this.lastAiInteraction.timestamp,
|
||||||
|
success: this.lastAiInteraction.success,
|
||||||
|
...(this.lastAiInteraction.error && { error: this.lastAiInteraction.error }),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register route handlers
|
// Register route handlers
|
||||||
@@ -461,6 +487,7 @@ export class WorkerService {
|
|||||||
|
|
||||||
// Track whether generator failed with an unrecoverable error to prevent infinite restart loops
|
// Track whether generator failed with an unrecoverable error to prevent infinite restart loops
|
||||||
let hadUnrecoverableError = false;
|
let hadUnrecoverableError = false;
|
||||||
|
let sessionFailed = false;
|
||||||
|
|
||||||
logger.info('SYSTEM', `Starting generator (${source}) using ${providerName}`, { sessionId: sid });
|
logger.info('SYSTEM', `Starting generator (${source}) using ${providerName}`, { sessionId: sid });
|
||||||
|
|
||||||
@@ -478,6 +505,12 @@ export class WorkerService {
|
|||||||
];
|
];
|
||||||
if (unrecoverablePatterns.some(pattern => errorMessage.includes(pattern))) {
|
if (unrecoverablePatterns.some(pattern => errorMessage.includes(pattern))) {
|
||||||
hadUnrecoverableError = true;
|
hadUnrecoverableError = true;
|
||||||
|
this.lastAiInteraction = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
success: false,
|
||||||
|
provider: providerName,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
logger.error('SDK', 'Unrecoverable generator error - will NOT restart', {
|
logger.error('SDK', 'Unrecoverable generator error - will NOT restart', {
|
||||||
sessionId: session.sessionDbId,
|
sessionId: session.sessionDbId,
|
||||||
project: session.project,
|
project: session.project,
|
||||||
@@ -514,11 +547,27 @@ export class WorkerService {
|
|||||||
project: session.project,
|
project: session.project,
|
||||||
provider: providerName
|
provider: providerName
|
||||||
}, error as Error);
|
}, error as Error);
|
||||||
|
sessionFailed = true;
|
||||||
|
this.lastAiInteraction = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
success: false,
|
||||||
|
provider: providerName,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
throw error;
|
throw error;
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
session.generatorPromise = null;
|
session.generatorPromise = null;
|
||||||
|
|
||||||
|
// Record successful AI interaction if no error occurred
|
||||||
|
if (!sessionFailed && !hadUnrecoverableError) {
|
||||||
|
this.lastAiInteraction = {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
success: true,
|
||||||
|
provider: providerName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Do NOT restart after unrecoverable errors - prevents infinite loops
|
// Do NOT restart after unrecoverable errors - prevents infinite loops
|
||||||
if (hadUnrecoverableError) {
|
if (hadUnrecoverableError) {
|
||||||
logger.warn('SYSTEM', 'Skipping restart due to unrecoverable error', {
|
logger.warn('SYSTEM', 'Skipping restart due to unrecoverable error', {
|
||||||
|
|||||||
Reference in New Issue
Block a user