Fix 404 error on /api/context/inject during worker startup (#310)
* Initial plan * Fix worker service connection failed error by adding early context/inject route Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com> * Add integration test for context inject early access Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com> * Fix import path and improve test code style Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com> * Add clarifying comment about intentional code duplication Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com> * build: compile fix for /api/context/inject 404 error Compiled worker service and MCP server with the initialization race condition fix. Validation results: All tests passing, route available immediately on restart. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com> Co-authored-by: Alex Newman <thedotmack@gmail.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
+100
-26
@@ -60,9 +60,18 @@ export class WorkerService {
|
||||
private searchRoutes: SearchRoutes | null;
|
||||
private settingsRoutes: SettingsRoutes;
|
||||
|
||||
// Initialization tracking
|
||||
private initializationComplete: Promise<void>;
|
||||
private resolveInitialization!: () => void;
|
||||
|
||||
constructor() {
|
||||
this.app = express();
|
||||
|
||||
// Initialize the promise that will resolve when background initialization completes
|
||||
this.initializationComplete = new Promise((resolve) => {
|
||||
this.resolveInitialization = resolve;
|
||||
});
|
||||
|
||||
// Initialize domain services
|
||||
this.dbManager = new DatabaseManager();
|
||||
this.sessionManager = new SessionManager(this.dbManager);
|
||||
@@ -155,6 +164,60 @@ export class WorkerService {
|
||||
this.dataRoutes.setupRoutes(this.app);
|
||||
// searchRoutes is set up after database initialization in initializeBackground()
|
||||
this.settingsRoutes.setupRoutes(this.app);
|
||||
|
||||
// Register early handler for /api/context/inject to avoid 404 during startup
|
||||
// This handler waits for initialization to complete before delegating to SearchRoutes
|
||||
// NOTE: This duplicates logic from SearchRoutes.handleContextInject by design,
|
||||
// as we need the route available immediately before SearchRoutes is initialized
|
||||
this.app.get('/api/context/inject', async (req, res, next) => {
|
||||
try {
|
||||
// Wait for initialization to complete (with timeout)
|
||||
const timeoutMs = 30000; // 30 second timeout
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Initialization timeout')), timeoutMs)
|
||||
);
|
||||
|
||||
await Promise.race([this.initializationComplete, timeoutPromise]);
|
||||
|
||||
// If searchRoutes is still null after initialization, something went wrong
|
||||
if (!this.searchRoutes) {
|
||||
res.status(503).json({ error: 'Search routes not initialized' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Delegate to the proper handler by re-processing the request
|
||||
// Since we're already in the middleware chain, we need to call the handler directly
|
||||
const projectName = req.query.project as string;
|
||||
const useColors = req.query.colors === 'true';
|
||||
|
||||
if (!projectName) {
|
||||
res.status(400).json({ error: 'Project parameter is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Import context generator (runs in worker, has access to database)
|
||||
const { generateContext } = await import('./context-generator.js');
|
||||
|
||||
// Use project name as CWD (generateContext uses path.basename to get project)
|
||||
const cwd = `/context/${projectName}`;
|
||||
|
||||
// Generate context
|
||||
const contextText = await generateContext(
|
||||
{
|
||||
session_id: 'context-inject-' + Date.now(),
|
||||
cwd: cwd
|
||||
},
|
||||
useColors
|
||||
);
|
||||
|
||||
// Return as plain text
|
||||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
res.send(contextText);
|
||||
} catch (error) {
|
||||
logger.error('WORKER', 'Context inject handler failed', {}, error as Error);
|
||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Internal server error' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -228,36 +291,47 @@ export class WorkerService {
|
||||
* Background initialization - runs after HTTP server is listening
|
||||
*/
|
||||
private async initializeBackground(): Promise<void> {
|
||||
// Clean up any orphaned chroma-mcp processes BEFORE starting our own
|
||||
await this.cleanupOrphanedProcesses();
|
||||
try {
|
||||
// Clean up any orphaned chroma-mcp processes BEFORE starting our own
|
||||
await this.cleanupOrphanedProcesses();
|
||||
|
||||
// Initialize database (once, stays open)
|
||||
await this.dbManager.initialize();
|
||||
// Initialize database (once, stays open)
|
||||
await this.dbManager.initialize();
|
||||
|
||||
// Initialize search services (requires initialized database)
|
||||
const formattingService = new FormattingService();
|
||||
const timelineService = new TimelineService();
|
||||
const searchManager = new SearchManager(
|
||||
this.dbManager.getSessionSearch(),
|
||||
this.dbManager.getSessionStore(),
|
||||
this.dbManager.getChromaSync(),
|
||||
formattingService,
|
||||
timelineService
|
||||
);
|
||||
this.searchRoutes = new SearchRoutes(searchManager);
|
||||
this.searchRoutes.setupRoutes(this.app); // Setup search routes now that SearchManager is ready
|
||||
logger.info('WORKER', 'SearchManager initialized and search routes registered');
|
||||
// Initialize search services (requires initialized database)
|
||||
const formattingService = new FormattingService();
|
||||
const timelineService = new TimelineService();
|
||||
const searchManager = new SearchManager(
|
||||
this.dbManager.getSessionSearch(),
|
||||
this.dbManager.getSessionStore(),
|
||||
this.dbManager.getChromaSync(),
|
||||
formattingService,
|
||||
timelineService
|
||||
);
|
||||
this.searchRoutes = new SearchRoutes(searchManager);
|
||||
this.searchRoutes.setupRoutes(this.app); // Setup search routes now that SearchManager is ready
|
||||
logger.info('WORKER', 'SearchManager initialized and search routes registered');
|
||||
|
||||
// Connect to MCP server
|
||||
const mcpServerPath = path.join(__dirname, 'mcp-server.cjs');
|
||||
const transport = new StdioClientTransport({
|
||||
command: 'node',
|
||||
args: [mcpServerPath],
|
||||
env: process.env
|
||||
});
|
||||
// Connect to MCP server
|
||||
const mcpServerPath = path.join(__dirname, 'mcp-server.cjs');
|
||||
const transport = new StdioClientTransport({
|
||||
command: 'node',
|
||||
args: [mcpServerPath],
|
||||
env: process.env
|
||||
});
|
||||
|
||||
await this.mcpClient.connect(transport);
|
||||
logger.success('WORKER', 'Connected to MCP server');
|
||||
await this.mcpClient.connect(transport);
|
||||
logger.success('WORKER', 'Connected to MCP server');
|
||||
|
||||
// Signal that initialization is complete
|
||||
this.resolveInitialization();
|
||||
logger.info('SYSTEM', 'Background initialization complete');
|
||||
} catch (error) {
|
||||
logger.error('SYSTEM', 'Background initialization failed', {}, error as Error);
|
||||
// Still resolve to prevent hanging requests, but they'll see searchRoutes is null
|
||||
this.resolveInitialization();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Integration Test: Context Inject Early Access
|
||||
*
|
||||
* Tests that /api/context/inject endpoint is available immediately
|
||||
* when worker starts, even before background initialization completes.
|
||||
*
|
||||
* This prevents the 404 error described in the issue where the hook
|
||||
* tries to access the endpoint before SearchRoutes are registered.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('Context Inject Early Access', () => {
|
||||
const workerPath = path.join(__dirname, '../../plugin/scripts/worker-service.cjs');
|
||||
|
||||
it('should have /api/context/inject route available immediately on startup', async () => {
|
||||
// This test verifies the fix by checking that:
|
||||
// 1. The route exists immediately (no 404)
|
||||
// 2. The route waits for initialization before processing
|
||||
// 3. Requests don't fail with "Cannot GET /api/context/inject"
|
||||
|
||||
// The fix adds an early handler that:
|
||||
// - Registers the route in setupRoutes() (called during construction)
|
||||
// - Waits for initializationComplete promise
|
||||
// - Processes the request after initialization
|
||||
|
||||
// Since we can't easily spin up a full worker in tests,
|
||||
// we verify the code structure is correct by checking
|
||||
// the compiled output contains the necessary pieces
|
||||
|
||||
const workerCode = fs.readFileSync(workerPath, 'utf-8');
|
||||
|
||||
// Verify initialization promise exists
|
||||
expect(workerCode).toContain('initializationComplete');
|
||||
expect(workerCode).toContain('resolveInitialization');
|
||||
|
||||
// Verify early route handler is registered in setupRoutes
|
||||
expect(workerCode).toContain('/api/context/inject');
|
||||
expect(workerCode).toContain('Promise.race');
|
||||
|
||||
// Verify the promise is resolved after initialization
|
||||
expect(workerCode).toContain('this.resolveInitialization()');
|
||||
});
|
||||
|
||||
it('should handle timeout if initialization takes too long', () => {
|
||||
const workerCode = fs.readFileSync(workerPath, 'utf-8');
|
||||
|
||||
// Verify timeout protection (30 seconds)
|
||||
expect(workerCode).toContain('3e4'); // 30000 in scientific notation
|
||||
expect(workerCode).toContain('Initialization timeout');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user