Fix export/import feature: JSON format and project filtering
Two critical fixes for the memory export/import feature:
1. **SearchManager empty results bug**: The /api/search endpoint
with format=json now returns {observations: [], sessions: [],
prompts: []} when no results are found, instead of the MCP
protocol format. This fixes the export-memories script which
expects consistent JSON structure regardless of result count.
2. **Project filtering support**: Updated SessionStore methods
(getObservationsByIds, getSessionSummariesByIds, getUserPromptsByIds)
to accept and apply project filter parameter. This enables
proper project-based filtering during ChromaDB hybrid search
result hydration.
Testing:
- Export with results: ✅ 50 observations exported
- Export with empty results: ✅ Proper JSON structure
- Round-trip import: ✅ Duplicate prevention working
- Project filtering: ✅ claude-mem (51 obs) vs rad-mem (1 obs)
Fixes export/import feature blocking bugs.
This commit is contained in:
@@ -811,26 +811,73 @@ export class SessionStore {
|
|||||||
*/
|
*/
|
||||||
getObservationsByIds(
|
getObservationsByIds(
|
||||||
ids: number[],
|
ids: number[],
|
||||||
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
|
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number; project?: string; type?: string | string[]; concepts?: string | string[]; files?: string | string[] } = {}
|
||||||
): ObservationRecord[] {
|
): ObservationRecord[] {
|
||||||
if (ids.length === 0) return [];
|
if (ids.length === 0) return [];
|
||||||
|
|
||||||
const { orderBy = 'date_desc', limit } = options;
|
const { orderBy = 'date_desc', limit, project, type, concepts, files } = options;
|
||||||
const orderClause = orderBy === 'date_asc' ? 'ASC' : 'DESC';
|
const orderClause = orderBy === 'date_asc' ? 'ASC' : 'DESC';
|
||||||
const limitClause = limit ? `LIMIT ${limit}` : '';
|
const limitClause = limit ? `LIMIT ${limit}` : '';
|
||||||
|
|
||||||
// Build placeholders for IN clause
|
// Build placeholders for IN clause
|
||||||
const placeholders = ids.map(() => '?').join(',');
|
const placeholders = ids.map(() => '?').join(',');
|
||||||
|
const params: any[] = [...ids];
|
||||||
|
const additionalConditions: string[] = [];
|
||||||
|
|
||||||
|
// Apply project filter
|
||||||
|
if (project) {
|
||||||
|
additionalConditions.push('project = ?');
|
||||||
|
params.push(project);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply type filter
|
||||||
|
if (type) {
|
||||||
|
if (Array.isArray(type)) {
|
||||||
|
const typePlaceholders = type.map(() => '?').join(',');
|
||||||
|
additionalConditions.push(`type IN (${typePlaceholders})`);
|
||||||
|
params.push(...type);
|
||||||
|
} else {
|
||||||
|
additionalConditions.push('type = ?');
|
||||||
|
params.push(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply concepts filter
|
||||||
|
if (concepts) {
|
||||||
|
const conceptsList = Array.isArray(concepts) ? concepts : [concepts];
|
||||||
|
const conceptConditions = conceptsList.map(() => {
|
||||||
|
params.push();
|
||||||
|
return 'EXISTS (SELECT 1 FROM json_each(concepts) WHERE value = ?)';
|
||||||
|
});
|
||||||
|
params.push(...conceptsList);
|
||||||
|
additionalConditions.push(`(${conceptConditions.join(' OR ')})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply files filter
|
||||||
|
if (files) {
|
||||||
|
const filesList = Array.isArray(files) ? files : [files];
|
||||||
|
const fileConditions = filesList.map(() => {
|
||||||
|
return '(EXISTS (SELECT 1 FROM json_each(files_read) WHERE value LIKE ?) OR EXISTS (SELECT 1 FROM json_each(files_modified) WHERE value LIKE ?))';
|
||||||
|
});
|
||||||
|
filesList.forEach(file => {
|
||||||
|
params.push(`%${file}%`, `%${file}%`);
|
||||||
|
});
|
||||||
|
additionalConditions.push(`(${fileConditions.join(' OR ')})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause = additionalConditions.length > 0
|
||||||
|
? `WHERE id IN (${placeholders}) AND ${additionalConditions.join(' AND ')}`
|
||||||
|
: `WHERE id IN (${placeholders})`;
|
||||||
|
|
||||||
const stmt = this.db.prepare(`
|
const stmt = this.db.prepare(`
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM observations
|
FROM observations
|
||||||
WHERE id IN (${placeholders})
|
${whereClause}
|
||||||
ORDER BY created_at_epoch ${orderClause}
|
ORDER BY created_at_epoch ${orderClause}
|
||||||
${limitClause}
|
${limitClause}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
return stmt.all(...ids) as ObservationRecord[];
|
return stmt.all(...params) as ObservationRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1353,23 +1400,30 @@ export class SessionStore {
|
|||||||
*/
|
*/
|
||||||
getSessionSummariesByIds(
|
getSessionSummariesByIds(
|
||||||
ids: number[],
|
ids: number[],
|
||||||
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
|
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number; project?: string } = {}
|
||||||
): SessionSummaryRecord[] {
|
): SessionSummaryRecord[] {
|
||||||
if (ids.length === 0) return [];
|
if (ids.length === 0) return [];
|
||||||
|
|
||||||
const { orderBy = 'date_desc', limit } = options;
|
const { orderBy = 'date_desc', limit, project } = options;
|
||||||
const orderClause = orderBy === 'date_asc' ? 'ASC' : 'DESC';
|
const orderClause = orderBy === 'date_asc' ? 'ASC' : 'DESC';
|
||||||
const limitClause = limit ? `LIMIT ${limit}` : '';
|
const limitClause = limit ? `LIMIT ${limit}` : '';
|
||||||
const placeholders = ids.map(() => '?').join(',');
|
const placeholders = ids.map(() => '?').join(',');
|
||||||
|
const params: any[] = [...ids];
|
||||||
|
|
||||||
|
// Apply project filter
|
||||||
|
const whereClause = project
|
||||||
|
? `WHERE id IN (${placeholders}) AND project = ?`
|
||||||
|
: `WHERE id IN (${placeholders})`;
|
||||||
|
if (project) params.push(project);
|
||||||
|
|
||||||
const stmt = this.db.prepare(`
|
const stmt = this.db.prepare(`
|
||||||
SELECT * FROM session_summaries
|
SELECT * FROM session_summaries
|
||||||
WHERE id IN (${placeholders})
|
${whereClause}
|
||||||
ORDER BY created_at_epoch ${orderClause}
|
ORDER BY created_at_epoch ${orderClause}
|
||||||
${limitClause}
|
${limitClause}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
return stmt.all(...ids) as SessionSummaryRecord[];
|
return stmt.all(...params) as SessionSummaryRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1378,14 +1432,19 @@ export class SessionStore {
|
|||||||
*/
|
*/
|
||||||
getUserPromptsByIds(
|
getUserPromptsByIds(
|
||||||
ids: number[],
|
ids: number[],
|
||||||
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
|
options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number; project?: string } = {}
|
||||||
): UserPromptRecord[] {
|
): UserPromptRecord[] {
|
||||||
if (ids.length === 0) return [];
|
if (ids.length === 0) return [];
|
||||||
|
|
||||||
const { orderBy = 'date_desc', limit } = options;
|
const { orderBy = 'date_desc', limit, project } = options;
|
||||||
const orderClause = orderBy === 'date_asc' ? 'ASC' : 'DESC';
|
const orderClause = orderBy === 'date_asc' ? 'ASC' : 'DESC';
|
||||||
const limitClause = limit ? `LIMIT ${limit}` : '';
|
const limitClause = limit ? `LIMIT ${limit}` : '';
|
||||||
const placeholders = ids.map(() => '?').join(',');
|
const placeholders = ids.map(() => '?').join(',');
|
||||||
|
const params: any[] = [...ids];
|
||||||
|
|
||||||
|
// Apply project filter
|
||||||
|
const projectFilter = project ? 'AND s.project = ?' : '';
|
||||||
|
if (project) params.push(project);
|
||||||
|
|
||||||
const stmt = this.db.prepare(`
|
const stmt = this.db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -1394,12 +1453,12 @@ export class SessionStore {
|
|||||||
s.sdk_session_id
|
s.sdk_session_id
|
||||||
FROM user_prompts up
|
FROM user_prompts up
|
||||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||||
WHERE up.id IN (${placeholders})
|
WHERE up.id IN (${placeholders}) ${projectFilter}
|
||||||
ORDER BY up.created_at_epoch ${orderClause}
|
ORDER BY up.created_at_epoch ${orderClause}
|
||||||
${limitClause}
|
${limitClause}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
return stmt.all(...ids) as UserPromptRecord[];
|
return stmt.all(...params) as UserPromptRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -198,6 +198,13 @@ export class SearchManager {
|
|||||||
const totalResults = observations.length + sessions.length + prompts.length;
|
const totalResults = observations.length + sessions.length + prompts.length;
|
||||||
|
|
||||||
if (totalResults === 0) {
|
if (totalResults === 0) {
|
||||||
|
if (format === 'json') {
|
||||||
|
return {
|
||||||
|
observations: [],
|
||||||
|
sessions: [],
|
||||||
|
prompts: []
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
type: 'text' as const,
|
type: 'text' as const,
|
||||||
@@ -230,6 +237,15 @@ export class SearchManager {
|
|||||||
const limitedResults = allResults.slice(0, options.limit || 20);
|
const limitedResults = allResults.slice(0, options.limit || 20);
|
||||||
|
|
||||||
// Format based on requested format
|
// Format based on requested format
|
||||||
|
if (format === 'json') {
|
||||||
|
// Raw JSON format for exports
|
||||||
|
return {
|
||||||
|
observations,
|
||||||
|
sessions,
|
||||||
|
prompts
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let combinedText: string;
|
let combinedText: string;
|
||||||
if (format === 'index') {
|
if (format === 'index') {
|
||||||
const header = `Found ${totalResults} result(s) matching "${query}" (${observations.length} obs, ${sessions.length} sessions, ${prompts.length} prompts):\n\n`;
|
const header = `Found ${totalResults} result(s) matching "${query}" (${observations.length} obs, ${sessions.length} sessions, ${prompts.length} prompts):\n\n`;
|
||||||
|
|||||||
Reference in New Issue
Block a user