Skip to content
Merged
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
36 changes: 34 additions & 2 deletions src/rest/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,13 @@ route('GET', '/api/vitals', async (_req, res, _params, engine) => {

// Coherence
route('GET', '/api/coherence', async (_req, res, _params, engine) => {
const result = await invokeTool(engine, 'validate', {});
json(res, result);
const report = await invokeTool(engine, 'graph_report', {}) as Record<string, unknown>;
const total = typeof report['total_memories'] === 'number' ? report['total_memories'] : 0;
const orphans = typeof report['orphaned_concepts'] === 'number' ? report['orphaned_concepts'] : 0;
const edges = typeof report['total_edges'] === 'number' ? report['total_edges'] : 0;
// Fraction of connected memories (1.0 = fully coherent, 0.0 = all orphans).
const score = total > 0 ? (total - orphans) / total : 1;
json(res, { score, total_memories: total, orphaned_concepts: orphans, total_edges: edges });
});

// Home (aggregate)
Expand All @@ -247,6 +252,33 @@ route('GET', '/api/home', async (_req, res, _params, engine) => {
});
});

// Concepts (memories exposed under the dashboard's preferred name)
route('GET', '/api/concepts', async (req, res, _params, engine) => {
const url = new URL(req.url!, `http://localhost`);
const limit = parseInt(url.searchParams.get('limit') ?? '100', 10);
const offset = parseInt(url.searchParams.get('offset') ?? '0', 10);
Comment on lines +255 to +259
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

limit/offset are parsed with parseInt(...) but never validated. If the client sends ?limit= / ?limit=abc / negative values, parseInt yields NaN/negative and Array.slice(offset, offset + limit) will behave unexpectedly (e.g., empty results or slicing from the end). Consider validating these params (finite integers, offset >= 0, 0 < limit <= max) and returning a 400 on invalid input (or coercing to safe defaults).

Suggested change
// Concepts (memories exposed under the dashboard's preferred name)
route('GET', '/api/concepts', async (req, res, _params, engine) => {
const url = new URL(req.url!, `http://localhost`);
const limit = parseInt(url.searchParams.get('limit') ?? '100', 10);
const offset = parseInt(url.searchParams.get('offset') ?? '0', 10);
function parsePaginationParam(
value: string | null,
defaultValue: number,
name: string,
options: { min: number; max?: number },
): number {
if (value === null) {
return defaultValue;
}
if (!/^-?\d+$/.test(value)) {
throw new Error(`Invalid ${name}: must be an integer`);
}
const parsed = Number(value);
if (!Number.isSafeInteger(parsed)) {
throw new Error(`Invalid ${name}: must be a safe integer`);
}
if (parsed < options.min) {
throw new Error(`Invalid ${name}: must be >= ${options.min}`);
}
if (options.max !== undefined && parsed > options.max) {
throw new Error(`Invalid ${name}: must be <= ${options.max}`);
}
return parsed;
}
// Concepts (memories exposed under the dashboard's preferred name)
route('GET', '/api/concepts', async (req, res, _params, engine) => {
const url = new URL(req.url!, `http://localhost`);
let limit: number;
let offset: number;
try {
limit = parsePaginationParam(url.searchParams.get('limit'), 100, 'limit', { min: 1, max: 100 });
offset = parsePaginationParam(url.searchParams.get('offset'), 0, 'offset', { min: 0 });
} catch (error) {
return json(res, { error: error instanceof Error ? error.message : 'Invalid pagination parameters' }, 400);
}

Copilot uses AI. Check for mistakes.
const store = engine.ctx.namespaces.getStore();
const allMemories = await store.getAllMemories();

const concepts = allMemories
.slice(offset, offset + limit)
.map(m => ({
id: m.id,
name: m.name,
definition: m.definition,
category: m.category,
confidence: m.confidence,
salience: m.salience,
access_count: m.access_count,
tags: m.tags ?? [],
fsrs: m.fsrs,
created_at: m.created_at?.toISOString?.() ?? new Date().toISOString(),
updated_at: m.updated_at?.toISOString?.() ?? new Date().toISOString(),
Comment thread
idapixl marked this conversation as resolved.
}));

json(res, { concepts, total: allMemories.length });
Comment on lines +255 to +279
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint loads the entire memory table via getAllMemories() and then paginates in-memory with .slice(...). CortexStore.getAllMemories() is explicitly documented as “use with caution” for batch operations, so this can become a scalability bottleneck as memory count grows. If possible, add a store-level paginated fetch (e.g., getMemories({ limit, offset })) and use that here (and in /api/memories) so REST pagination avoids full-table reads.

Suggested change
// Concepts (memories exposed under the dashboard's preferred name)
route('GET', '/api/concepts', async (req, res, _params, engine) => {
const url = new URL(req.url!, `http://localhost`);
const limit = parseInt(url.searchParams.get('limit') ?? '100', 10);
const offset = parseInt(url.searchParams.get('offset') ?? '0', 10);
const store = engine.ctx.namespaces.getStore();
const allMemories = await store.getAllMemories();
const concepts = allMemories
.slice(offset, offset + limit)
.map(m => ({
id: m.id,
name: m.name,
definition: m.definition,
category: m.category,
confidence: m.confidence,
salience: m.salience,
access_count: m.access_count,
tags: m.tags ?? [],
fsrs: m.fsrs,
created_at: m.created_at?.toISOString?.() ?? new Date().toISOString(),
updated_at: m.updated_at?.toISOString?.() ?? new Date().toISOString(),
}));
json(res, { concepts, total: allMemories.length });
async function getConceptPage(
store: {
getAllMemories: () => Promise<any[]>;
getMemories?: (options: { limit: number; offset: number }) => Promise<any[] | { memories?: any[]; items?: any[]; total?: number; count?: number }>;
countMemories?: () => Promise<number>;
getMemoryCount?: () => Promise<number>;
},
limit: number,
offset: number,
): Promise<{ memories: any[]; total: number }> {
if (typeof store.getMemories === 'function') {
const paginated = await store.getMemories({ limit, offset });
if (Array.isArray(paginated)) {
if (typeof store.countMemories === 'function') {
return { memories: paginated, total: await store.countMemories() };
}
if (typeof store.getMemoryCount === 'function') {
return { memories: paginated, total: await store.getMemoryCount() };
}
return { memories: paginated, total: offset + paginated.length };
}
const memories = Array.isArray(paginated.memories)
? paginated.memories
: Array.isArray(paginated.items)
? paginated.items
: [];
if (typeof paginated.total === 'number') {
return { memories, total: paginated.total };
}
if (typeof paginated.count === 'number') {
return { memories, total: paginated.count };
}
if (typeof store.countMemories === 'function') {
return { memories, total: await store.countMemories() };
}
if (typeof store.getMemoryCount === 'function') {
return { memories, total: await store.getMemoryCount() };
}
return { memories, total: offset + memories.length };
}
const allMemories = await store.getAllMemories();
return {
memories: allMemories.slice(offset, offset + limit),
total: allMemories.length,
};
}
// Concepts (memories exposed under the dashboard's preferred name)
route('GET', '/api/concepts', async (req, res, _params, engine) => {
const url = new URL(req.url!, `http://localhost`);
const limit = parseInt(url.searchParams.get('limit') ?? '100', 10);
const offset = parseInt(url.searchParams.get('offset') ?? '0', 10);
const store = engine.ctx.namespaces.getStore();
const { memories, total } = await getConceptPage(store, limit, offset);
const concepts = memories.map(m => ({
id: m.id,
name: m.name,
definition: m.definition,
category: m.category,
confidence: m.confidence,
salience: m.salience,
access_count: m.access_count,
tags: m.tags ?? [],
fsrs: m.fsrs,
created_at: m.created_at?.toISOString?.() ?? new Date().toISOString(),
updated_at: m.updated_at?.toISOString?.() ?? new Date().toISOString(),
}));
json(res, { concepts, total });

Copilot uses AI. Check for mistakes.
});

// ── Memories ──────────────────────────────────────────────────────────────────

route('GET', '/api/memories', async (req, res, _params, engine) => {
Expand Down
Loading