Refactor session ID references from sdk_session_id to memory_session_id across multiple services and database queries

- Updated SQL queries in cleanup-duplicates.ts and context-generator.ts to use memory_session_id.
- Modified interfaces in context-generator.ts to reflect the new session ID naming.
- Implemented a repair migration in SessionStore.ts to rename columns in existing tables.
- Adjusted FormattingService.ts and SDKAgent.ts to utilize memory_session_id for session handling.
- Ensured SearchManager.ts retrieves summaries and observations using the updated memory_session_id.
This commit is contained in:
Alex Newman
2025-12-28 23:13:47 -05:00
parent d7c183b3e1
commit 656a7f7c0f
10 changed files with 229 additions and 148 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -13,7 +13,7 @@ ${n.stack}`:` ${n.message}`:this.getLevel()===0&&typeof n=="object"?h=`
Available tools: ${Object.keys(ds).join(", ")}`}],isError:!0};let t=ds[e];return{content:[{type:"text",text:`# ${e} Parameters
${JSON.stringify(t,null,2)}`}]}}},{name:"search",description:'Search memory. All parameters optional - call get_schema("search") for details',inputSchema:{type:"object",properties:{},additionalProperties:!0},handler:async a=>{let e=Pt.search;return await xt(e,a)}},{name:"timeline",description:'Timeline context. All parameters optional - call get_schema("timeline") for details',inputSchema:{type:"object",properties:{},additionalProperties:!0},handler:async a=>{let e=Pt.timeline;return await xt(e,a)}},{name:"get_recent_context",description:'Recent context. All parameters optional - call get_schema("get_recent_context") for details',inputSchema:{type:"object",properties:{},additionalProperties:!0},handler:async a=>{let e=Pt.get_recent_context;return await xt(e,a)}},{name:"get_context_timeline",description:"Timeline around observation ID",inputSchema:{type:"object",properties:{anchor:{type:"number",description:'Observation ID (required). Optional params: get_schema("get_context_timeline")'}},required:["anchor"],additionalProperties:!0},handler:async a=>{let e=Pt.get_context_timeline;return await xt(e,a)}},{name:"help",description:'Get detailed docs. All parameters optional - call get_schema("help") for details',inputSchema:{type:"object",properties:{},additionalProperties:!0},handler:async a=>{let e=Pt.help;return await xt(e,a)}},{name:"get_observation",description:"Fetch observation by ID",inputSchema:{type:"object",properties:{id:{type:"number",description:"Observation ID (required)"}},required:["id"]},handler:async a=>await fs("/api/observation",a.id)},{name:"get_observations",description:"Batch fetch observations",inputSchema:{type:"object",properties:{ids:{type:"array",items:{type:"number"},description:'Array of observation IDs (required). Optional params: get_schema("get_observations")'}},required:["ids"],additionalProperties:!0},handler:async a=>await Ad("/api/observations/batch",a)},{name:"get_session",description:"Fetch session by ID",inputSchema:{type:"object",properties:{id:{type:"number",description:"Session ID (required)"}},required:["id"]},handler:async a=>await fs("/api/session",a.id)},{name:"get_prompt",description:"Fetch prompt by ID",inputSchema:{type:"object",properties:{id:{type:"number",description:"Prompt ID (required)"}},required:["id"]},handler:async a=>await fs("/api/prompt",a.id)}],hs=new la({name:"mem-search-server",version:"1.0.0"},{capabilities:{tools:{}}});hs.setRequestHandler(Da,async()=>({tools:mo.map(a=>({name:a.name,description:a.description,inputSchema:a.inputSchema}))}));hs.setRequestHandler(ka,async a=>{let e=mo.find(t=>t.name===a.params.name);if(!e)throw new Error(`Unknown tool: ${a.params.name}`);try{return await e.handler(a.params.arguments||{})}catch(t){return{content:[{type:"text",text:`Tool execution failed: ${t.message}`}],isError:!0}}});async function vo(){fe.info("SYSTEM","MCP server shutting down"),process.exit(0)}process.on("SIGTERM",vo);process.on("SIGINT",vo);async function $d(){let a=new ua;await hs.connect(a),fe.info("SYSTEM","Claude-mem search server started"),setTimeout(async()=>{await Cd()?fe.info("SYSTEM","Worker available",void 0,{workerUrl:ot}):(fe.warn("SYSTEM","Worker not available",void 0,{workerUrl:ot}),fe.warn("SYSTEM","Tools will fail until Worker is started"),fe.warn("SYSTEM","Start Worker with: npm run worker:restart"))},0)}$d().catch(a=>{fe.error("SYSTEM","Fatal error",void 0,a),process.exit(1)});
${JSON.stringify(t,null,2)}`}]}}},{name:"search",description:'Search memory. All parameters optional - call get_schema("search") for details',inputSchema:{type:"object",properties:{},additionalProperties:!0},handler:async a=>{let e=Pt.search;return await xt(e,a)}},{name:"timeline",description:'Timeline context. All parameters optional - call get_schema("timeline") for details',inputSchema:{type:"object",properties:{},additionalProperties:!0},handler:async a=>{let e=Pt.timeline;return await xt(e,a)}},{name:"get_recent_context",description:'Recent context. All parameters optional - call get_schema("get_recent_context") for details',inputSchema:{type:"object",properties:{},additionalProperties:!0},handler:async a=>{let e=Pt.get_recent_context;return await xt(e,a)}},{name:"get_context_timeline",description:"Timeline around observation ID",inputSchema:{type:"object",properties:{anchor:{type:"number",description:'Observation ID (required). Optional params: get_schema("get_context_timeline")'}},required:["anchor"],additionalProperties:!0},handler:async a=>{let e=Pt.get_context_timeline;return await xt(e,a)}},{name:"help",description:'Get detailed docs. All parameters optional - call get_schema("help") for details',inputSchema:{type:"object",properties:{},additionalProperties:!0},handler:async a=>{let e=Pt.help;return await xt(e,a)}},{name:"get_observation",description:"Fetch observation by ID",inputSchema:{type:"object",properties:{id:{type:"number",description:"Observation ID (required)"}},required:["id"]},handler:async a=>await fs("/api/observation",a.id)},{name:"get_observations",description:"Batch fetch observations",inputSchema:{type:"object",properties:{ids:{type:"array",items:{type:"number"},description:'Array of observation IDs (required). Optional params: get_schema("get_observations")'}},required:["ids"],additionalProperties:!0},handler:async a=>await Ad("/api/observations/batch",a)},{name:"get_session",description:"Fetch session by ID",inputSchema:{type:"object",properties:{id:{type:"number",description:"Session ID (required)"}},required:["id"]},handler:async a=>await fs("/api/session",a.id)},{name:"get_prompt",description:"Fetch prompt by ID",inputSchema:{type:"object",properties:{id:{type:"number",description:"Prompt ID (required)"}},required:["id"]},handler:async a=>await fs("/api/prompt",a.id)}],hs=new la({name:"mcp-search-server",version:"1.0.0"},{capabilities:{tools:{}}});hs.setRequestHandler(Da,async()=>({tools:mo.map(a=>({name:a.name,description:a.description,inputSchema:a.inputSchema}))}));hs.setRequestHandler(ka,async a=>{let e=mo.find(t=>t.name===a.params.name);if(!e)throw new Error(`Unknown tool: ${a.params.name}`);try{return await e.handler(a.params.arguments||{})}catch(t){return{content:[{type:"text",text:`Tool execution failed: ${t.message}`}],isError:!0}}});async function vo(){fe.info("SYSTEM","MCP server shutting down"),process.exit(0)}process.on("SIGTERM",vo);process.on("SIGINT",vo);async function $d(){let a=new ua;await hs.connect(a),fe.info("SYSTEM","Claude-mem search server started"),setTimeout(async()=>{await Cd()?fe.info("SYSTEM","Worker available",void 0,{workerUrl:ot}):(fe.warn("SYSTEM","Worker not available",void 0,{workerUrl:ot}),fe.warn("SYSTEM","Tools will fail until Worker is started"),fe.warn("SYSTEM","Start Worker with: npm run worker:restart"))},0)}$d().catch(a=>{fe.error("SYSTEM","Fatal error",void 0,a),process.exit(1)});
/*! Bundled license information:
uri-js/dist/es5/uri.all.js:
File diff suppressed because one or more lines are too long
Binary file not shown.
+6 -6
View File
@@ -15,14 +15,14 @@ function main() {
console.log('Finding duplicate observations...');
const duplicateObsQuery = db['db'].prepare(`
SELECT sdk_session_id, title, subtitle, type, COUNT(*) as count, GROUP_CONCAT(id) as ids
SELECT memory_session_id, title, subtitle, type, COUNT(*) as count, GROUP_CONCAT(id) as ids
FROM observations
GROUP BY sdk_session_id, title, subtitle, type
GROUP BY memory_session_id, title, subtitle, type
HAVING count > 1
`);
const duplicateObs = duplicateObsQuery.all() as Array<{
sdk_session_id: string;
memory_session_id: string;
title: string;
subtitle: string;
type: string;
@@ -50,14 +50,14 @@ function main() {
console.log('\n\nFinding duplicate summaries...');
const duplicateSumQuery = db['db'].prepare(`
SELECT sdk_session_id, request, completed, learned, COUNT(*) as count, GROUP_CONCAT(id) as ids
SELECT memory_session_id, request, completed, learned, COUNT(*) as count, GROUP_CONCAT(id) as ids
FROM session_summaries
GROUP BY sdk_session_id, request, completed, learned
GROUP BY memory_session_id, request, completed, learned
HAVING count > 1
`);
const duplicateSum = duplicateSumQuery.all() as Array<{
sdk_session_id: string;
memory_session_id: string;
request: string;
completed: string;
learned: string;
+6 -6
View File
@@ -122,7 +122,7 @@ const colors = {
interface Observation {
id: number;
sdk_session_id: string;
memory_session_id: string;
type: string;
title: string | null;
subtitle: string | null;
@@ -138,7 +138,7 @@ interface Observation {
interface SessionSummary {
id: number;
sdk_session_id: string;
memory_session_id: string;
request: string | null;
investigated: string | null;
learned: string | null;
@@ -246,7 +246,7 @@ export async function generateContext(input?: ContextInput, useColors: boolean =
// Get recent observations
const observations = db.db.prepare(`
SELECT
id, sdk_session_id, type, title, subtitle, narrative,
id, memory_session_id, type, title, subtitle, narrative,
facts, concepts, files_read, files_modified, discovery_tokens,
created_at, created_at_epoch
FROM observations
@@ -262,7 +262,7 @@ export async function generateContext(input?: ContextInput, useColors: boolean =
// Get recent summaries
const recentSummaries = db.db.prepare(`
SELECT id, sdk_session_id, request, investigated, learned, completed, next_steps, created_at, created_at_epoch
SELECT id, memory_session_id, request, investigated, learned, completed, next_steps, created_at, created_at_epoch
FROM session_summaries
WHERE project = ?
ORDER BY created_at_epoch DESC
@@ -275,10 +275,10 @@ export async function generateContext(input?: ContextInput, useColors: boolean =
if (config.showLastMessage && observations.length > 0) {
const currentSessionId = input?.session_id;
const priorSessionObs = observations.find(obs => obs.sdk_session_id !== currentSessionId);
const priorSessionObs = observations.find(obs => obs.memory_session_id !== currentSessionId);
if (priorSessionObs) {
const priorSessionId = priorSessionObs.sdk_session_id;
const priorSessionId = priorSessionObs.memory_session_id;
const dashedCwd = cwdToDashed(cwd);
const transcriptPath = path.join(homedir(), '.claude', 'projects', dashedCwd, `${priorSessionId}.jsonl`);
const messages = extractPriorMessages(transcriptPath);
+74
View File
@@ -44,6 +44,7 @@ export class SessionStore {
this.ensureDiscoveryTokensColumn();
this.createPendingMessagesTable();
this.renameSessionIdColumns();
this.repairSessionIdColumnRename();
}
/**
@@ -626,6 +627,75 @@ export class SessionStore {
}
}
/**
* Repair session ID column renames (migration 19)
* Migration 17 may have been recorded but failed to actually rename columns.
* This migration checks each table and renames if needed (idempotent).
*/
private repairSessionIdColumnRename(): void {
try {
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(19) as SchemaVersion | undefined;
if (applied) return;
logger.info('DB', 'Checking session ID column renames (repair migration)');
let repairsNeeded = false;
// Check and fix sdk_sessions
const sessionsInfo = this.db.query('PRAGMA table_info(sdk_sessions)').all() as TableColumnInfo[];
if (sessionsInfo.some(col => col.name === 'claude_session_id')) {
logger.info('DB', 'Repairing sdk_sessions columns');
this.db.run('ALTER TABLE sdk_sessions RENAME COLUMN claude_session_id TO content_session_id');
this.db.run('ALTER TABLE sdk_sessions RENAME COLUMN sdk_session_id TO memory_session_id');
repairsNeeded = true;
}
// Check and fix pending_messages
const pendingInfo = this.db.query('PRAGMA table_info(pending_messages)').all() as TableColumnInfo[];
if (pendingInfo.some(col => col.name === 'claude_session_id')) {
logger.info('DB', 'Repairing pending_messages columns');
this.db.run('ALTER TABLE pending_messages RENAME COLUMN claude_session_id TO content_session_id');
repairsNeeded = true;
}
// Check and fix observations
const obsInfo = this.db.query('PRAGMA table_info(observations)').all() as TableColumnInfo[];
if (obsInfo.some(col => col.name === 'sdk_session_id')) {
logger.info('DB', 'Repairing observations columns');
this.db.run('ALTER TABLE observations RENAME COLUMN sdk_session_id TO memory_session_id');
repairsNeeded = true;
}
// Check and fix session_summaries
const summariesInfo = this.db.query('PRAGMA table_info(session_summaries)').all() as TableColumnInfo[];
if (summariesInfo.some(col => col.name === 'sdk_session_id')) {
logger.info('DB', 'Repairing session_summaries columns');
this.db.run('ALTER TABLE session_summaries RENAME COLUMN sdk_session_id TO memory_session_id');
repairsNeeded = true;
}
// Check and fix user_prompts
const promptsInfo = this.db.query('PRAGMA table_info(user_prompts)').all() as TableColumnInfo[];
if (promptsInfo.some(col => col.name === 'claude_session_id')) {
logger.info('DB', 'Repairing user_prompts columns');
this.db.run('ALTER TABLE user_prompts RENAME COLUMN claude_session_id TO content_session_id');
repairsNeeded = true;
}
// Record migration
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(19, new Date().toISOString());
if (repairsNeeded) {
logger.info('DB', 'Session ID column rename repairs completed');
} else {
logger.info('DB', 'No session ID column repairs needed');
}
} catch (error: any) {
logger.error('DB', 'Session ID column rename repair error', undefined, error);
throw error;
}
}
/**
* Update the memory session ID for a session
* Called by SDKAgent when it captures the session ID from the first SDK message
@@ -1147,6 +1217,10 @@ export class SessionStore {
const nowEpoch = now.getTime();
// Pure INSERT OR IGNORE - no updates, no complexity
// NOTE: memory_session_id is initialized to contentSessionId as a placeholder for FK purposes.
// The REAL memory session ID is captured by SDKAgent from the first SDK response
// and stored via updateMemorySessionId(). The resume logic checks if memorySessionId
// differs from contentSessionId before using it - see SDKAgent.startSession().
this.db.prepare(`
INSERT OR IGNORE INTO sdk_sessions
(content_session_id, memory_session_id, project, user_prompt, started_at, started_at_epoch, status)
+2 -2
View File
@@ -74,7 +74,7 @@ Tips:
const id = `#S${session.id}`;
const time = this.formatTime(session.created_at_epoch);
const icon = '🎯';
const title = session.request || `Session ${session.sdk_session_id?.substring(0, 8) || 'unknown'}`;
const title = session.request || `Session ${session.memory_session_id?.substring(0, 8) || 'unknown'}`;
return `| ${id} | ${time} | ${icon} | ${title} | - | - |`;
}
@@ -137,7 +137,7 @@ Tips:
const id = `#S${session.id}`;
const time = this.formatTime(session.created_at_epoch);
const icon = '🎯';
const title = session.request || `Session ${session.sdk_session_id?.substring(0, 8) || 'unknown'}`;
const title = session.request || `Session ${session.memory_session_id?.substring(0, 8) || 'unknown'}`;
// Use ditto mark if same time as previous row
const timeDisplay = time === lastTime ? '″' : time;
+11 -4
View File
@@ -64,22 +64,29 @@ export class SDKAgent {
// Create message generator (event-driven)
const messageGenerator = this.createMessageGenerator(session);
// CRITICAL: Only resume if memorySessionId is a REAL captured SDK session ID,
// not the placeholder (which equals contentSessionId). The placeholder is set
// for FK purposes but would cause the bug where we try to resume the USER's session!
const hasRealMemorySessionId = session.memorySessionId &&
session.memorySessionId !== session.contentSessionId;
logger.info('SDK', 'Starting SDK query', {
sessionDbId: session.sessionDbId,
contentSessionId: session.contentSessionId,
memorySessionId: session.memorySessionId,
resume_parameter: session.memorySessionId || '(none - fresh start)',
hasRealMemorySessionId,
resume_parameter: hasRealMemorySessionId ? session.memorySessionId : '(none - fresh start)',
lastPromptNumber: session.lastPromptNumber
});
// Run Agent SDK query loop
// Use memorySessionId for resume (captured from previous SDK response) if available
// Only resume if we have a REAL captured memory session ID (not the placeholder)
const queryResult = query({
prompt: messageGenerator,
options: {
model: modelId,
// Only resume if we have a captured memory session ID from previous SDK interaction
...(session.memorySessionId && { resume: session.memorySessionId }),
// Only resume if memorySessionId differs from contentSessionId (meaning it was captured)
...(hasRealMemorySessionId && { resume: session.memorySessionId }),
disallowedTools,
abortController: session.abortController,
pathToClaudeCodeExecutable: claudePath
+3 -3
View File
@@ -1376,13 +1376,13 @@ export class SearchManager {
lines.push('');
for (const session of sessions) {
if (!session.sdk_session_id) continue;
if (!session.memory_session_id) continue;
lines.push('---');
lines.push('');
if (session.has_summary) {
const summary = this.sessionStore.getSummaryForSession(session.sdk_session_id);
const summary = this.sessionStore.getSummaryForSession(session.memory_session_id);
if (summary) {
const promptLabel = summary.prompt_number ? ` (Prompt #${summary.prompt_number})` : '';
lines.push(`**Summary${promptLabel}**`);
@@ -1432,7 +1432,7 @@ export class SearchManager {
lines.push(`**Request:** ${session.user_prompt}`);
}
const observations = this.sessionStore.getObservationsForSession(session.sdk_session_id);
const observations = this.sessionStore.getObservationsForSession(session.memory_session_id);
if (observations.length > 0) {
lines.push('');
lines.push(`**Observations (${observations.length}):**`);