diff --git a/src/cortex/pipeline.ts b/src/cortex/pipeline.ts index faae207..13cde69 100644 --- a/src/cortex/pipeline.ts +++ b/src/cortex/pipeline.ts @@ -123,11 +123,13 @@ export class CortexPipeline { const totalSentences = allSentences.length; - // Step 2: Score sentences via semantic highlighting (concurrent per memory) + // Step 2: Score sentences via semantic highlighting (bounded concurrency per memory) if (this.highlighter) { - // Concurrent highlight calls: all memories in parallel instead of sequential - const highlightResults = await Promise.all( - memories.map(memory => this.highlighter!.highlight(query, memory.content, 0)) + // Bounded concurrent highlight calls to avoid resource exhaustion + const highlightResults = await limitConcurrency( + memories, + memory => this.highlighter!.highlight(query, memory.content, 0), + 5 ); // Map scores back to the allSentences array for (let mi = 0; mi < memories.length; mi++) { @@ -239,6 +241,18 @@ export class CortexPipeline { } } +/** + * Run async operations with bounded concurrency to prevent resource exhaustion + */ +async function limitConcurrency(items: T[], fn: (item: T) => Promise, limit = 5): Promise { + const results: R[] = []; + for (let i = 0; i < items.length; i += limit) { + const chunk = items.slice(i, i + limit); + results.push(...await Promise.all(chunk.map(fn))); + } + return results; +} + /** * Split text into sentences (shared utility) */ diff --git a/src/mcp/auth/scopes.ts b/src/mcp/auth/scopes.ts index 6bbf360..333066e 100644 --- a/src/mcp/auth/scopes.ts +++ b/src/mcp/auth/scopes.ts @@ -58,7 +58,7 @@ export const ToolScopes: Record = { titan_focus_add: [Scopes.WRITE], titan_focus_remove: [Scopes.WRITE], titan_focus_clear: [Scopes.WRITE], - titan_noop: [Scopes.WRITE], + titan_noop: [Scopes.READ], titan_intent: [Scopes.WRITE], // Admin operations diff --git a/src/mcp/http-server.ts b/src/mcp/http-server.ts index d9fd57c..269ebc4 100644 --- a/src/mcp/http-server.ts +++ b/src/mcp/http-server.ts @@ -164,7 +164,17 @@ export function createHttpApp(config: HttpServerConfig = {}): Express { // Fail-closed: refuse to start without any authentication mode if (!authMiddleware) { - throw new Error('No authentication mode configured. Set AUTH0_DOMAIN + AUTH0_AUDIENCE, or enable allowLocalhostBypass.'); + console.error('[titan-memory] FATAL: No authentication mode configured. Set AUTH0_DOMAIN + AUTH0_AUDIENCE, or enable allowLocalhostBypass.'); + console.error('[titan-memory] The server will respond 503 to all /mcp requests until auth is configured.'); + + // Return a degraded server that responds 503 instead of crashing the process + app.use('/mcp', (_req: Request, res: Response) => { + res.status(503).json({ + error: 'Service Unavailable', + message: 'No authentication mode configured. Set AUTH0_DOMAIN + AUTH0_AUDIENCE, or enable allowLocalhostBypass.', + }); + }); + return app; } // MCP endpoint with auth diff --git a/src/titan.ts b/src/titan.ts index ad004d5..108109d 100644 --- a/src/titan.ts +++ b/src/titan.ts @@ -691,14 +691,20 @@ export class TitanMemory { const limit = options?.limit || 10; const mode = options?.mode || 'full'; - // Query all target layers in parallel - const queryPromises = targetLayers.map(layerId => { - const layer = this.layers.get(layerId); - if (!layer) return Promise.resolve(null); - return layer.query(query, { ...options, limit: limit * 2 }); // Get extra for fusion - }); + // Query all target layers with bounded concurrency + const allResults: (QueryResult | null)[] = []; + const concurrencyLimit = 5; + for (let i = 0; i < targetLayers.length; i += concurrencyLimit) { + const chunk = targetLayers.slice(i, i + concurrencyLimit); + const chunkResults = await Promise.all(chunk.map(layerId => { + const layer = this.layers.get(layerId); + if (!layer) return Promise.resolve(null); + return layer.query(query, { ...options, limit: limit * 2 }); // Get extra for fusion + })); + allResults.push(...chunkResults); + } - const results = (await Promise.all(queryPromises)).filter( + const results = allResults.filter( (r): r is QueryResult => r !== null ); @@ -1725,12 +1731,18 @@ export class TitanMemory { })); } - // Concurrent highlight calls: all memories in parallel instead of sequential - const highlightResults = await Promise.all( - memories.map(memory => - this.semanticHighlighter!.highlight(query, memory.content, threshold) - ) - ); + // Bounded concurrent highlight calls to avoid resource exhaustion + const highlightResults: Awaited>[] = []; + const highlightLimit = 5; + for (let i = 0; i < memories.length; i += highlightLimit) { + const chunk = memories.slice(i, i + highlightLimit); + const chunkResults = await Promise.all( + chunk.map(memory => + this.semanticHighlighter!.highlight(query, memory.content, threshold) + ) + ); + highlightResults.push(...chunkResults); + } return memories.map((memory, i) => { const highlighted = highlightResults[i]; diff --git a/src/utils/auth.ts b/src/utils/auth.ts index 6263115..cbb115f 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -28,8 +28,9 @@ const DEFAULT_AUTH_CONFIG: AuthConfig = { let authConfig: AuthConfig = { ...DEFAULT_AUTH_CONFIG }; -// Startup warning: auth enabled but no tokens configured -if (DEFAULT_AUTH_CONFIG.enabled && +// Startup warning: auth enabled but no tokens configured (suppress in test environments) +if (process.env.NODE_ENV !== 'test' && + DEFAULT_AUTH_CONFIG.enabled && DEFAULT_AUTH_CONFIG.dashboardTokens.length === 0 && DEFAULT_AUTH_CONFIG.a2aTokens.length === 0) { console.error('[titan-memory] WARNING: Auth is enabled but no TITAN_DASHBOARD_TOKENS or TITAN_A2A_TOKENS are configured. All authenticated requests will be rejected unless using localhost bypass.');