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
22 changes: 18 additions & 4 deletions src/cortex/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand Down Expand Up @@ -239,6 +241,18 @@ export class CortexPipeline {
}
}

/**
* Run async operations with bounded concurrency to prevent resource exhaustion
*/
async function limitConcurrency<T, R>(items: T[], fn: (item: T) => Promise<R>, limit = 5): Promise<R[]> {
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)
*/
Expand Down
2 changes: 1 addition & 1 deletion src/mcp/auth/scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const ToolScopes: Record<string, Scope[]> = {
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
Expand Down
12 changes: 11 additions & 1 deletion src/mcp/http-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 25 additions & 13 deletions src/titan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);

Expand Down Expand Up @@ -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<ReturnType<typeof this.semanticHighlighter.highlight>>[] = [];
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];
Expand Down
5 changes: 3 additions & 2 deletions src/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down
Loading