fix(session): Semantic renaming and memory session ID capture for resume
This commit fixes the session ID confusion identified in PR #475: PROBLEM: - Using contentSessionId (user's Claude Code session) for SDK resume was wrong - Memory agent conversation should persist across the entire user session - Each SDK call was starting fresh, losing memory agent continuity SOLUTION: 1. Semantic Renaming (clarity): - claudeSessionId → contentSessionId (user's observed session) - sdkSessionId → memorySessionId (memory agent's session for resume) - Database migration 17 renames columns accordingly 2. Memory Session ID Capture: - SDKAgent captures session_id from first SDK message - Persists to database via updateMemorySessionId() - SessionManager loads memorySessionId on session init 3. Resume Logic Fixed: - Only resume if memorySessionId captured from previous interaction - Enables memory agent continuity across user prompts Files changed: 33 (types, database, agents, hooks, routes) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ import { logger } from '../../utils/logger.js';
|
||||
export interface PersistentPendingMessage {
|
||||
id: number;
|
||||
session_db_id: number;
|
||||
claude_session_id: string;
|
||||
content_session_id: string;
|
||||
message_type: 'observation' | 'summarize';
|
||||
tool_name: string | null;
|
||||
tool_input: string | null;
|
||||
@@ -53,11 +53,11 @@ export class PendingMessageStore {
|
||||
* Enqueue a new message (persist before processing)
|
||||
* @returns The database ID of the persisted message
|
||||
*/
|
||||
enqueue(sessionDbId: number, claudeSessionId: string, message: PendingMessage): number {
|
||||
enqueue(sessionDbId: number, contentSessionId: string, message: PendingMessage): number {
|
||||
const now = Date.now();
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO pending_messages (
|
||||
session_db_id, claude_session_id, message_type,
|
||||
session_db_id, content_session_id, message_type,
|
||||
tool_name, tool_input, tool_response, cwd,
|
||||
last_user_message, last_assistant_message,
|
||||
prompt_number, status, retry_count, created_at_epoch
|
||||
@@ -66,7 +66,7 @@ export class PendingMessageStore {
|
||||
|
||||
const result = stmt.run(
|
||||
sessionDbId,
|
||||
claudeSessionId,
|
||||
contentSessionId,
|
||||
message.type,
|
||||
message.tool_name || null,
|
||||
message.tool_input ? JSON.stringify(message.tool_input) : null,
|
||||
@@ -140,7 +140,7 @@ export class PendingMessageStore {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT pm.*, ss.project
|
||||
FROM pending_messages pm
|
||||
LEFT JOIN sdk_sessions ss ON pm.claude_session_id = ss.claude_session_id
|
||||
LEFT JOIN sdk_sessions ss ON pm.content_session_id = ss.content_session_id
|
||||
WHERE pm.status IN ('pending', 'processing', 'failed')
|
||||
ORDER BY
|
||||
CASE pm.status
|
||||
@@ -226,7 +226,7 @@ export class PendingMessageStore {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT pm.*, ss.project
|
||||
FROM pending_messages pm
|
||||
LEFT JOIN sdk_sessions ss ON pm.claude_session_id = ss.claude_session_id
|
||||
LEFT JOIN sdk_sessions ss ON pm.content_session_id = ss.content_session_id
|
||||
WHERE pm.status = 'processed' AND pm.completed_at_epoch > ?
|
||||
ORDER BY pm.completed_at_epoch DESC
|
||||
LIMIT ?
|
||||
@@ -354,12 +354,12 @@ export class PendingMessageStore {
|
||||
/**
|
||||
* Get session info for a pending message (for recovery)
|
||||
*/
|
||||
getSessionInfoForMessage(messageId: number): { sessionDbId: number; claudeSessionId: string } | null {
|
||||
getSessionInfoForMessage(messageId: number): { sessionDbId: number; contentSessionId: string } | null {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT session_db_id, claude_session_id FROM pending_messages WHERE id = ?
|
||||
SELECT session_db_id, content_session_id FROM pending_messages WHERE id = ?
|
||||
`);
|
||||
const result = stmt.get(messageId) as { session_db_id: number; claude_session_id: string } | undefined;
|
||||
return result ? { sessionDbId: result.session_db_id, claudeSessionId: result.claude_session_id } : null;
|
||||
const result = stmt.get(messageId) as { session_db_id: number; content_session_id: string } | undefined;
|
||||
return result ? { sessionDbId: result.session_db_id, contentSessionId: result.content_session_id } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -481,7 +481,7 @@ export class SessionSearch {
|
||||
const sql = `
|
||||
SELECT up.*
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
|
||||
${whereClause}
|
||||
${orderClause}
|
||||
LIMIT ? OFFSET ?
|
||||
@@ -498,23 +498,23 @@ export class SessionSearch {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all prompts for a session by claude_session_id
|
||||
* Get all prompts for a session by content_session_id
|
||||
*/
|
||||
getUserPromptsBySession(claudeSessionId: string): UserPromptRow[] {
|
||||
getUserPromptsBySession(contentSessionId: string): UserPromptRow[] {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
id,
|
||||
claude_session_id,
|
||||
content_session_id,
|
||||
prompt_number,
|
||||
prompt_text,
|
||||
created_at,
|
||||
created_at_epoch
|
||||
FROM user_prompts
|
||||
WHERE claude_session_id = ?
|
||||
WHERE content_session_id = ?
|
||||
ORDER BY prompt_number ASC
|
||||
`);
|
||||
|
||||
return stmt.all(claudeSessionId) as UserPromptRow[];
|
||||
return stmt.all(contentSessionId) as UserPromptRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+189
-127
@@ -43,6 +43,7 @@ export class SessionStore {
|
||||
this.createUserPromptsTable();
|
||||
this.ensureDiscoveryTokensColumn();
|
||||
this.createPendingMessagesTable();
|
||||
this.renameSessionIdColumns();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,8 +74,8 @@ export class SessionStore {
|
||||
this.db.run(`
|
||||
CREATE TABLE IF NOT EXISTS sdk_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
sdk_session_id TEXT UNIQUE,
|
||||
content_session_id TEXT UNIQUE NOT NULL,
|
||||
memory_session_id TEXT UNIQUE,
|
||||
project TEXT NOT NULL,
|
||||
user_prompt TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
@@ -84,31 +85,31 @@ export class SessionStore {
|
||||
status TEXT CHECK(status IN ('active', 'completed', 'failed')) NOT NULL DEFAULT 'active'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(claude_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(content_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(memory_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_project ON sdk_sessions(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_status ON sdk_sessions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_started ON sdk_sessions(started_at_epoch DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
memory_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')),
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(memory_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS session_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT UNIQUE NOT NULL,
|
||||
memory_session_id TEXT UNIQUE NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
@@ -120,10 +121,10 @@ export class SessionStore {
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(memory_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`);
|
||||
@@ -200,7 +201,7 @@ export class SessionStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove UNIQUE constraint from session_summaries.sdk_session_id (migration 7)
|
||||
* Remove UNIQUE constraint from session_summaries.memory_session_id (migration 7)
|
||||
*/
|
||||
private removeSessionSummariesUniqueConstraint(): void {
|
||||
// Check if migration already applied
|
||||
@@ -217,7 +218,7 @@ export class SessionStore {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('DB', 'Removing UNIQUE constraint from session_summaries.sdk_session_id');
|
||||
logger.info('DB', 'Removing UNIQUE constraint from session_summaries.memory_session_id');
|
||||
|
||||
// Begin transaction
|
||||
this.db.run('BEGIN TRANSACTION');
|
||||
@@ -227,7 +228,7 @@ export class SessionStore {
|
||||
this.db.run(`
|
||||
CREATE TABLE session_summaries_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
memory_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
@@ -240,14 +241,14 @@ export class SessionStore {
|
||||
prompt_number INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Copy data from old table
|
||||
this.db.run(`
|
||||
INSERT INTO session_summaries_new
|
||||
SELECT id, sdk_session_id, project, request, investigated, learned,
|
||||
SELECT id, memory_session_id, project, request, investigated, learned,
|
||||
completed, next_steps, files_read, files_edited, notes,
|
||||
prompt_number, created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
@@ -261,7 +262,7 @@ export class SessionStore {
|
||||
|
||||
// Recreate indexes
|
||||
this.db.run(`
|
||||
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(memory_session_id);
|
||||
CREATE INDEX idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`);
|
||||
@@ -272,7 +273,7 @@ export class SessionStore {
|
||||
// Record migration
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(7, new Date().toISOString());
|
||||
|
||||
logger.info('DB', 'Successfully removed UNIQUE constraint from session_summaries.sdk_session_id');
|
||||
logger.info('DB', 'Successfully removed UNIQUE constraint from session_summaries.memory_session_id');
|
||||
} catch (error: any) {
|
||||
// Rollback on error
|
||||
this.db.run('ROLLBACK');
|
||||
@@ -346,7 +347,7 @@ export class SessionStore {
|
||||
this.db.run(`
|
||||
CREATE TABLE observations_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
memory_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
text TEXT,
|
||||
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
|
||||
@@ -360,14 +361,14 @@ export class SessionStore {
|
||||
prompt_number INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
// Copy data from old table (all existing columns)
|
||||
this.db.run(`
|
||||
INSERT INTO observations_new
|
||||
SELECT id, sdk_session_id, project, text, type, title, subtitle, facts,
|
||||
SELECT id, memory_session_id, project, text, type, title, subtitle, facts,
|
||||
narrative, concepts, files_read, files_modified, prompt_number,
|
||||
created_at, created_at_epoch
|
||||
FROM observations
|
||||
@@ -381,7 +382,7 @@ export class SessionStore {
|
||||
|
||||
// Recreate indexes
|
||||
this.db.run(`
|
||||
CREATE INDEX idx_observations_sdk_session ON observations(sdk_session_id);
|
||||
CREATE INDEX idx_observations_sdk_session ON observations(memory_session_id);
|
||||
CREATE INDEX idx_observations_project ON observations(project);
|
||||
CREATE INDEX idx_observations_type ON observations(type);
|
||||
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
|
||||
@@ -423,22 +424,22 @@ export class SessionStore {
|
||||
this.db.run('BEGIN TRANSACTION');
|
||||
|
||||
try {
|
||||
// Create main table (using claude_session_id since sdk_session_id is set asynchronously by worker)
|
||||
// Create main table (using content_session_id since memory_session_id is set asynchronously by worker)
|
||||
this.db.run(`
|
||||
CREATE TABLE user_prompts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
content_session_id TEXT NOT NULL,
|
||||
prompt_number INTEGER NOT NULL,
|
||||
prompt_text TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(claude_session_id) REFERENCES sdk_sessions(claude_session_id) ON DELETE CASCADE
|
||||
FOREIGN KEY(content_session_id) REFERENCES sdk_sessions(content_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_prompts_claude_session ON user_prompts(claude_session_id);
|
||||
CREATE INDEX idx_user_prompts_claude_session ON user_prompts(content_session_id);
|
||||
CREATE INDEX idx_user_prompts_created ON user_prompts(created_at_epoch DESC);
|
||||
CREATE INDEX idx_user_prompts_prompt_number ON user_prompts(prompt_number);
|
||||
CREATE INDEX idx_user_prompts_lookup ON user_prompts(claude_session_id, prompt_number);
|
||||
CREATE INDEX idx_user_prompts_lookup ON user_prompts(content_session_id, prompt_number);
|
||||
`);
|
||||
|
||||
// Create FTS5 virtual table
|
||||
@@ -545,7 +546,7 @@ export class SessionStore {
|
||||
CREATE TABLE pending_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_db_id INTEGER NOT NULL,
|
||||
claude_session_id TEXT NOT NULL,
|
||||
content_session_id TEXT NOT NULL,
|
||||
message_type TEXT NOT NULL CHECK(message_type IN ('observation', 'summarize')),
|
||||
tool_name TEXT,
|
||||
tool_input TEXT,
|
||||
@@ -565,7 +566,7 @@ export class SessionStore {
|
||||
|
||||
this.db.run('CREATE INDEX IF NOT EXISTS idx_pending_messages_session ON pending_messages(session_db_id)');
|
||||
this.db.run('CREATE INDEX IF NOT EXISTS idx_pending_messages_status ON pending_messages(status)');
|
||||
this.db.run('CREATE INDEX IF NOT EXISTS idx_pending_messages_claude_session ON pending_messages(claude_session_id)');
|
||||
this.db.run('CREATE INDEX IF NOT EXISTS idx_pending_messages_claude_session ON pending_messages(content_session_id)');
|
||||
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(16, new Date().toISOString());
|
||||
|
||||
@@ -576,6 +577,67 @@ export class SessionStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename session ID columns for semantic clarity (migration 17)
|
||||
* - claude_session_id → content_session_id (user's observed session)
|
||||
* - sdk_session_id → memory_session_id (memory agent's session for resume)
|
||||
*/
|
||||
private renameSessionIdColumns(): void {
|
||||
try {
|
||||
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(17) as SchemaVersion | undefined;
|
||||
if (applied) return;
|
||||
|
||||
logger.info('DB', 'Renaming session ID columns for semantic clarity');
|
||||
|
||||
// Check if columns are already renamed (idempotent check)
|
||||
const sessionsInfo = this.db.query('PRAGMA table_info(sdk_sessions)').all() as TableColumnInfo[];
|
||||
const hasContentSessionId = sessionsInfo.some(col => col.name === 'content_session_id');
|
||||
|
||||
if (hasContentSessionId) {
|
||||
// Already renamed, just record migration
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(17, new Date().toISOString());
|
||||
return;
|
||||
}
|
||||
|
||||
// SQLite 3.25+ supports ALTER TABLE RENAME COLUMN
|
||||
// Rename in sdk_sessions table
|
||||
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');
|
||||
|
||||
// Rename in pending_messages table
|
||||
this.db.run('ALTER TABLE pending_messages RENAME COLUMN claude_session_id TO content_session_id');
|
||||
|
||||
// Rename in observations table
|
||||
this.db.run('ALTER TABLE observations RENAME COLUMN sdk_session_id TO memory_session_id');
|
||||
|
||||
// Rename in session_summaries table
|
||||
this.db.run('ALTER TABLE session_summaries RENAME COLUMN sdk_session_id TO memory_session_id');
|
||||
|
||||
// Rename in user_prompts table
|
||||
this.db.run('ALTER TABLE user_prompts RENAME COLUMN claude_session_id TO content_session_id');
|
||||
|
||||
// Record migration
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(17, new Date().toISOString());
|
||||
|
||||
logger.info('DB', 'Successfully renamed session ID columns');
|
||||
} catch (error: any) {
|
||||
logger.error('DB', 'Session ID column rename migration 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
|
||||
*/
|
||||
updateMemorySessionId(sessionDbId: number, memorySessionId: string): void {
|
||||
this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET memory_session_id = ?
|
||||
WHERE id = ?
|
||||
`).run(memorySessionId, sessionDbId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent session summaries for a project
|
||||
*/
|
||||
@@ -608,7 +670,7 @@ export class SessionStore {
|
||||
* Get recent summaries with session info for context display
|
||||
*/
|
||||
getRecentSummariesWithSessionInfo(project: string, limit: number = 3): Array<{
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string;
|
||||
request: string | null;
|
||||
learned: string | null;
|
||||
completed: string | null;
|
||||
@@ -618,7 +680,7 @@ export class SessionStore {
|
||||
}> {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
sdk_session_id, request, learned, completed, next_steps,
|
||||
memory_session_id, request, learned, completed, next_steps,
|
||||
prompt_number, created_at
|
||||
FROM session_summaries
|
||||
WHERE project = ?
|
||||
@@ -708,7 +770,7 @@ export class SessionStore {
|
||||
*/
|
||||
getAllRecentUserPrompts(limit: number = 100): Array<{
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
content_session_id: string;
|
||||
project: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
@@ -718,14 +780,14 @@ export class SessionStore {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
up.id,
|
||||
up.claude_session_id,
|
||||
up.content_session_id,
|
||||
s.project,
|
||||
up.prompt_number,
|
||||
up.prompt_text,
|
||||
up.created_at,
|
||||
up.created_at_epoch
|
||||
FROM user_prompts up
|
||||
LEFT JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
LEFT JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
|
||||
ORDER BY up.created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
@@ -752,10 +814,10 @@ export class SessionStore {
|
||||
* Get latest user prompt with session info for a Claude session
|
||||
* Used for syncing prompts to Chroma during session initialization
|
||||
*/
|
||||
getLatestUserPrompt(claudeSessionId: string): {
|
||||
getLatestUserPrompt(contentSessionId: string): {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
sdk_session_id: string;
|
||||
content_session_id: string;
|
||||
memory_session_id: string;
|
||||
project: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
@@ -764,23 +826,23 @@ export class SessionStore {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
up.*,
|
||||
s.sdk_session_id,
|
||||
s.memory_session_id,
|
||||
s.project
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.claude_session_id = ?
|
||||
JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
|
||||
WHERE up.content_session_id = ?
|
||||
ORDER BY up.created_at_epoch DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
return stmt.get(claudeSessionId) as LatestPromptResult | undefined;
|
||||
return stmt.get(contentSessionId) as LatestPromptResult | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent sessions with their status and summary info
|
||||
*/
|
||||
getRecentSessionsWithStatus(project: string, limit: number = 3): Array<{
|
||||
sdk_session_id: string | null;
|
||||
memory_session_id: string | null;
|
||||
status: string;
|
||||
started_at: string;
|
||||
user_prompt: string | null;
|
||||
@@ -789,16 +851,16 @@ export class SessionStore {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
s.sdk_session_id,
|
||||
s.memory_session_id,
|
||||
s.status,
|
||||
s.started_at,
|
||||
s.started_at_epoch,
|
||||
s.user_prompt,
|
||||
CASE WHEN sum.sdk_session_id IS NOT NULL THEN 1 ELSE 0 END as has_summary
|
||||
CASE WHEN sum.memory_session_id IS NOT NULL THEN 1 ELSE 0 END as has_summary
|
||||
FROM sdk_sessions s
|
||||
LEFT JOIN session_summaries sum ON s.sdk_session_id = sum.sdk_session_id
|
||||
WHERE s.project = ? AND s.sdk_session_id IS NOT NULL
|
||||
GROUP BY s.sdk_session_id
|
||||
LEFT JOIN session_summaries sum ON s.memory_session_id = sum.memory_session_id
|
||||
WHERE s.project = ? AND s.memory_session_id IS NOT NULL
|
||||
GROUP BY s.memory_session_id
|
||||
ORDER BY s.started_at_epoch DESC
|
||||
LIMIT ?
|
||||
)
|
||||
@@ -811,7 +873,7 @@ export class SessionStore {
|
||||
/**
|
||||
* Get observations for a specific session
|
||||
*/
|
||||
getObservationsForSession(sdkSessionId: string): Array<{
|
||||
getObservationsForSession(memorySessionId: string): Array<{
|
||||
title: string;
|
||||
subtitle: string;
|
||||
type: string;
|
||||
@@ -820,11 +882,11 @@ export class SessionStore {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT title, subtitle, type, prompt_number
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
WHERE memory_session_id = ?
|
||||
ORDER BY created_at_epoch ASC
|
||||
`);
|
||||
|
||||
return stmt.all(sdkSessionId);
|
||||
return stmt.all(memorySessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -916,7 +978,7 @@ export class SessionStore {
|
||||
/**
|
||||
* Get summary for a specific session
|
||||
*/
|
||||
getSummaryForSession(sdkSessionId: string): {
|
||||
getSummaryForSession(memorySessionId: string): {
|
||||
request: string | null;
|
||||
investigated: string | null;
|
||||
learned: string | null;
|
||||
@@ -935,28 +997,28 @@ export class SessionStore {
|
||||
files_read, files_edited, notes, prompt_number, created_at,
|
||||
created_at_epoch
|
||||
FROM session_summaries
|
||||
WHERE sdk_session_id = ?
|
||||
WHERE memory_session_id = ?
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
return stmt.get(sdkSessionId) || null;
|
||||
return stmt.get(memorySessionId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregated files from all observations for a session
|
||||
*/
|
||||
getFilesForSession(sdkSessionId: string): {
|
||||
getFilesForSession(memorySessionId: string): {
|
||||
filesRead: string[];
|
||||
filesModified: string[];
|
||||
} {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT files_read, files_modified
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
WHERE memory_session_id = ?
|
||||
`);
|
||||
|
||||
const rows = stmt.all(sdkSessionId) as Array<{
|
||||
const rows = stmt.all(memorySessionId) as Array<{
|
||||
files_read: string | null;
|
||||
files_modified: string | null;
|
||||
}>;
|
||||
@@ -993,13 +1055,13 @@ export class SessionStore {
|
||||
*/
|
||||
getSessionById(id: number): {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
sdk_session_id: string | null;
|
||||
content_session_id: string;
|
||||
memory_session_id: string | null;
|
||||
project: string;
|
||||
user_prompt: string;
|
||||
} | null {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT id, claude_session_id, sdk_session_id, project, user_prompt
|
||||
SELECT id, content_session_id, memory_session_id, project, user_prompt
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
@@ -1012,10 +1074,10 @@ export class SessionStore {
|
||||
* Get SDK sessions by SDK session IDs
|
||||
* Used for exporting session metadata
|
||||
*/
|
||||
getSdkSessionsBySessionIds(sdkSessionIds: string[]): {
|
||||
getSdkSessionsBySessionIds(memorySessionIds: string[]): {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
sdk_session_id: string;
|
||||
content_session_id: string;
|
||||
memory_session_id: string;
|
||||
project: string;
|
||||
user_prompt: string;
|
||||
started_at: string;
|
||||
@@ -1024,18 +1086,18 @@ export class SessionStore {
|
||||
completed_at_epoch: number | null;
|
||||
status: string;
|
||||
}[] {
|
||||
if (sdkSessionIds.length === 0) return [];
|
||||
if (memorySessionIds.length === 0) return [];
|
||||
|
||||
const placeholders = sdkSessionIds.map(() => '?').join(',');
|
||||
const placeholders = memorySessionIds.map(() => '?').join(',');
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT id, claude_session_id, sdk_session_id, project, user_prompt,
|
||||
SELECT id, content_session_id, memory_session_id, project, user_prompt,
|
||||
started_at, started_at_epoch, completed_at, completed_at_epoch, status
|
||||
FROM sdk_sessions
|
||||
WHERE sdk_session_id IN (${placeholders})
|
||||
WHERE memory_session_id IN (${placeholders})
|
||||
ORDER BY started_at_epoch DESC
|
||||
`);
|
||||
|
||||
return stmt.all(...sdkSessionIds) as any[];
|
||||
return stmt.all(...memorySessionIds) as any[];
|
||||
}
|
||||
|
||||
|
||||
@@ -1047,10 +1109,10 @@ export class SessionStore {
|
||||
* Get current prompt number by counting user_prompts for this session
|
||||
* Replaces the prompt_counter column which is no longer maintained
|
||||
*/
|
||||
getPromptNumberFromUserPrompts(claudeSessionId: string): number {
|
||||
getPromptNumberFromUserPrompts(contentSessionId: string): number {
|
||||
const result = this.db.prepare(`
|
||||
SELECT COUNT(*) as count FROM user_prompts WHERE claude_session_id = ?
|
||||
`).get(claudeSessionId) as { count: number };
|
||||
SELECT COUNT(*) as count FROM user_prompts WHERE content_session_id = ?
|
||||
`).get(contentSessionId) as { count: number };
|
||||
return result.count;
|
||||
}
|
||||
|
||||
@@ -1080,20 +1142,20 @@ export class SessionStore {
|
||||
* This is KISS in action: Trust the database UNIQUE constraint and
|
||||
* INSERT OR IGNORE to handle both creation and lookup elegantly.
|
||||
*/
|
||||
createSDKSession(claudeSessionId: string, project: string, userPrompt: string): number {
|
||||
createSDKSession(contentSessionId: string, project: string, userPrompt: string): number {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
// Pure INSERT OR IGNORE - no updates, no complexity
|
||||
this.db.prepare(`
|
||||
INSERT OR IGNORE INTO sdk_sessions
|
||||
(claude_session_id, sdk_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
(content_session_id, memory_session_id, project, user_prompt, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(claudeSessionId, claudeSessionId, project, userPrompt, now.toISOString(), nowEpoch);
|
||||
`).run(contentSessionId, contentSessionId, project, userPrompt, now.toISOString(), nowEpoch);
|
||||
|
||||
// Return existing or new ID
|
||||
const row = this.db.prepare('SELECT id FROM sdk_sessions WHERE claude_session_id = ?')
|
||||
.get(claudeSessionId) as { id: number };
|
||||
const row = this.db.prepare('SELECT id FROM sdk_sessions WHERE content_session_id = ?')
|
||||
.get(contentSessionId) as { id: number };
|
||||
return row.id;
|
||||
}
|
||||
|
||||
@@ -1103,17 +1165,17 @@ export class SessionStore {
|
||||
/**
|
||||
* Save a user prompt
|
||||
*/
|
||||
saveUserPrompt(claudeSessionId: string, promptNumber: number, promptText: string): number {
|
||||
saveUserPrompt(contentSessionId: string, promptNumber: number, promptText: string): number {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO user_prompts
|
||||
(claude_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
(content_session_id, prompt_number, prompt_text, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(claudeSessionId, promptNumber, promptText, now.toISOString(), nowEpoch);
|
||||
const result = stmt.run(contentSessionId, promptNumber, promptText, now.toISOString(), nowEpoch);
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
|
||||
@@ -1121,15 +1183,15 @@ export class SessionStore {
|
||||
* Get user prompt by session ID and prompt number
|
||||
* Returns the prompt text, or null if not found
|
||||
*/
|
||||
getUserPrompt(claudeSessionId: string, promptNumber: number): string | null {
|
||||
getUserPrompt(contentSessionId: string, promptNumber: number): string | null {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT prompt_text
|
||||
FROM user_prompts
|
||||
WHERE claude_session_id = ? AND prompt_number = ?
|
||||
WHERE content_session_id = ? AND prompt_number = ?
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
const result = stmt.get(claudeSessionId, promptNumber) as { prompt_text: string } | undefined;
|
||||
const result = stmt.get(contentSessionId, promptNumber) as { prompt_text: string } | undefined;
|
||||
return result?.prompt_text ?? null;
|
||||
}
|
||||
|
||||
@@ -1138,7 +1200,7 @@ export class SessionStore {
|
||||
* Assumes session already exists (created by hook)
|
||||
*/
|
||||
storeObservation(
|
||||
sdkSessionId: string,
|
||||
memorySessionId: string,
|
||||
project: string,
|
||||
observation: {
|
||||
type: string;
|
||||
@@ -1160,13 +1222,13 @@ export class SessionStore {
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO observations
|
||||
(sdk_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
(memory_session_id, project, type, title, subtitle, facts, narrative, concepts,
|
||||
files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
sdkSessionId,
|
||||
memorySessionId,
|
||||
project,
|
||||
observation.type,
|
||||
observation.title,
|
||||
@@ -1193,7 +1255,7 @@ export class SessionStore {
|
||||
* Assumes session already exists - will fail with FK error if not
|
||||
*/
|
||||
storeSummary(
|
||||
sdkSessionId: string,
|
||||
memorySessionId: string,
|
||||
project: string,
|
||||
summary: {
|
||||
request: string;
|
||||
@@ -1213,13 +1275,13 @@ export class SessionStore {
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO session_summaries
|
||||
(sdk_session_id, project, request, investigated, learned, completed,
|
||||
(memory_session_id, project, request, investigated, learned, completed,
|
||||
next_steps, notes, prompt_number, discovery_tokens, created_at, created_at_epoch)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
sdkSessionId,
|
||||
memorySessionId,
|
||||
project,
|
||||
summary.request,
|
||||
summary.investigated,
|
||||
@@ -1302,9 +1364,9 @@ export class SessionStore {
|
||||
SELECT
|
||||
up.*,
|
||||
s.project,
|
||||
s.sdk_session_id
|
||||
s.memory_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
|
||||
WHERE up.id IN (${placeholders}) ${projectFilter}
|
||||
ORDER BY up.created_at_epoch ${orderClause}
|
||||
${limitClause}
|
||||
@@ -1437,9 +1499,9 @@ export class SessionStore {
|
||||
`;
|
||||
|
||||
const promptQuery = `
|
||||
SELECT up.*, s.project, s.sdk_session_id
|
||||
SELECT up.*, s.project, s.memory_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${projectFilter.replace('project', 's.project')}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;
|
||||
@@ -1453,7 +1515,7 @@ export class SessionStore {
|
||||
observations,
|
||||
sessions: sessions.map(s => ({
|
||||
id: s.id,
|
||||
sdk_session_id: s.sdk_session_id,
|
||||
memory_session_id: s.memory_session_id,
|
||||
project: s.project,
|
||||
request: s.request,
|
||||
completed: s.completed,
|
||||
@@ -1463,7 +1525,7 @@ export class SessionStore {
|
||||
})),
|
||||
prompts: prompts.map(p => ({
|
||||
id: p.id,
|
||||
claude_session_id: p.claude_session_id,
|
||||
content_session_id: p.content_session_id,
|
||||
prompt_number: p.prompt_number,
|
||||
prompt_text: p.prompt_text,
|
||||
project: p.project,
|
||||
@@ -1482,7 +1544,7 @@ export class SessionStore {
|
||||
*/
|
||||
getPromptById(id: number): {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
content_session_id: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
project: string;
|
||||
@@ -1492,14 +1554,14 @@ export class SessionStore {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
p.id,
|
||||
p.claude_session_id,
|
||||
p.content_session_id,
|
||||
p.prompt_number,
|
||||
p.prompt_text,
|
||||
s.project,
|
||||
p.created_at,
|
||||
p.created_at_epoch
|
||||
FROM user_prompts p
|
||||
LEFT JOIN sdk_sessions s ON p.claude_session_id = s.claude_session_id
|
||||
LEFT JOIN sdk_sessions s ON p.content_session_id = s.content_session_id
|
||||
WHERE p.id = ?
|
||||
LIMIT 1
|
||||
`);
|
||||
@@ -1512,7 +1574,7 @@ export class SessionStore {
|
||||
*/
|
||||
getPromptsByIds(ids: number[]): Array<{
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
content_session_id: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
project: string;
|
||||
@@ -1525,21 +1587,21 @@ export class SessionStore {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
p.id,
|
||||
p.claude_session_id,
|
||||
p.content_session_id,
|
||||
p.prompt_number,
|
||||
p.prompt_text,
|
||||
s.project,
|
||||
p.created_at,
|
||||
p.created_at_epoch
|
||||
FROM user_prompts p
|
||||
LEFT JOIN sdk_sessions s ON p.claude_session_id = s.claude_session_id
|
||||
LEFT JOIN sdk_sessions s ON p.content_session_id = s.content_session_id
|
||||
WHERE p.id IN (${placeholders})
|
||||
ORDER BY p.created_at_epoch DESC
|
||||
`);
|
||||
|
||||
return stmt.all(...ids) as Array<{
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
content_session_id: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
project: string;
|
||||
@@ -1553,8 +1615,8 @@ export class SessionStore {
|
||||
*/
|
||||
getSessionSummaryById(id: number): {
|
||||
id: number;
|
||||
sdk_session_id: string | null;
|
||||
claude_session_id: string;
|
||||
memory_session_id: string | null;
|
||||
content_session_id: string;
|
||||
project: string;
|
||||
user_prompt: string;
|
||||
request_summary: string | null;
|
||||
@@ -1566,8 +1628,8 @@ export class SessionStore {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT
|
||||
id,
|
||||
sdk_session_id,
|
||||
claude_session_id,
|
||||
memory_session_id,
|
||||
content_session_id,
|
||||
project,
|
||||
user_prompt,
|
||||
request_summary,
|
||||
@@ -1599,8 +1661,8 @@ export class SessionStore {
|
||||
* Returns: { imported: boolean, id: number }
|
||||
*/
|
||||
importSdkSession(session: {
|
||||
claude_session_id: string;
|
||||
sdk_session_id: string;
|
||||
content_session_id: string;
|
||||
memory_session_id: string;
|
||||
project: string;
|
||||
user_prompt: string;
|
||||
started_at: string;
|
||||
@@ -1611,8 +1673,8 @@ export class SessionStore {
|
||||
}): { imported: boolean; id: number } {
|
||||
// Check if session already exists
|
||||
const existing = this.db.prepare(
|
||||
'SELECT id FROM sdk_sessions WHERE claude_session_id = ?'
|
||||
).get(session.claude_session_id) as { id: number } | undefined;
|
||||
'SELECT id FROM sdk_sessions WHERE content_session_id = ?'
|
||||
).get(session.content_session_id) as { id: number } | undefined;
|
||||
|
||||
if (existing) {
|
||||
return { imported: false, id: existing.id };
|
||||
@@ -1620,14 +1682,14 @@ export class SessionStore {
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO sdk_sessions (
|
||||
claude_session_id, sdk_session_id, project, user_prompt,
|
||||
content_session_id, memory_session_id, project, user_prompt,
|
||||
started_at, started_at_epoch, completed_at, completed_at_epoch, status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
session.claude_session_id,
|
||||
session.sdk_session_id,
|
||||
session.content_session_id,
|
||||
session.memory_session_id,
|
||||
session.project,
|
||||
session.user_prompt,
|
||||
session.started_at,
|
||||
@@ -1645,7 +1707,7 @@ export class SessionStore {
|
||||
* Returns: { imported: boolean, id: number }
|
||||
*/
|
||||
importSessionSummary(summary: {
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string;
|
||||
project: string;
|
||||
request: string | null;
|
||||
investigated: string | null;
|
||||
@@ -1662,8 +1724,8 @@ export class SessionStore {
|
||||
}): { imported: boolean; id: number } {
|
||||
// Check if summary already exists for this session
|
||||
const existing = this.db.prepare(
|
||||
'SELECT id FROM session_summaries WHERE sdk_session_id = ?'
|
||||
).get(summary.sdk_session_id) as { id: number } | undefined;
|
||||
'SELECT id FROM session_summaries WHERE memory_session_id = ?'
|
||||
).get(summary.memory_session_id) as { id: number } | undefined;
|
||||
|
||||
if (existing) {
|
||||
return { imported: false, id: existing.id };
|
||||
@@ -1671,14 +1733,14 @@ export class SessionStore {
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO session_summaries (
|
||||
sdk_session_id, project, request, investigated, learned,
|
||||
memory_session_id, project, request, investigated, learned,
|
||||
completed, next_steps, files_read, files_edited, notes,
|
||||
prompt_number, discovery_tokens, created_at, created_at_epoch
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
summary.sdk_session_id,
|
||||
summary.memory_session_id,
|
||||
summary.project,
|
||||
summary.request,
|
||||
summary.investigated,
|
||||
@@ -1699,11 +1761,11 @@ export class SessionStore {
|
||||
|
||||
/**
|
||||
* Import observation with duplicate checking
|
||||
* Duplicates are identified by sdk_session_id + title + created_at_epoch
|
||||
* Duplicates are identified by memory_session_id + title + created_at_epoch
|
||||
* Returns: { imported: boolean, id: number }
|
||||
*/
|
||||
importObservation(obs: {
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string;
|
||||
project: string;
|
||||
text: string | null;
|
||||
type: string;
|
||||
@@ -1722,8 +1784,8 @@ export class SessionStore {
|
||||
// Check if observation already exists
|
||||
const existing = this.db.prepare(`
|
||||
SELECT id FROM observations
|
||||
WHERE sdk_session_id = ? AND title = ? AND created_at_epoch = ?
|
||||
`).get(obs.sdk_session_id, obs.title, obs.created_at_epoch) as { id: number } | undefined;
|
||||
WHERE memory_session_id = ? AND title = ? AND created_at_epoch = ?
|
||||
`).get(obs.memory_session_id, obs.title, obs.created_at_epoch) as { id: number } | undefined;
|
||||
|
||||
if (existing) {
|
||||
return { imported: false, id: existing.id };
|
||||
@@ -1731,14 +1793,14 @@ export class SessionStore {
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO observations (
|
||||
sdk_session_id, project, text, type, title, subtitle,
|
||||
memory_session_id, project, text, type, title, subtitle,
|
||||
facts, narrative, concepts, files_read, files_modified,
|
||||
prompt_number, discovery_tokens, created_at, created_at_epoch
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
obs.sdk_session_id,
|
||||
obs.memory_session_id,
|
||||
obs.project,
|
||||
obs.text,
|
||||
obs.type,
|
||||
@@ -1760,11 +1822,11 @@ export class SessionStore {
|
||||
|
||||
/**
|
||||
* Import user prompt with duplicate checking
|
||||
* Duplicates are identified by claude_session_id + prompt_number
|
||||
* Duplicates are identified by content_session_id + prompt_number
|
||||
* Returns: { imported: boolean, id: number }
|
||||
*/
|
||||
importUserPrompt(prompt: {
|
||||
claude_session_id: string;
|
||||
content_session_id: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
created_at: string;
|
||||
@@ -1773,8 +1835,8 @@ export class SessionStore {
|
||||
// Check if prompt already exists
|
||||
const existing = this.db.prepare(`
|
||||
SELECT id FROM user_prompts
|
||||
WHERE claude_session_id = ? AND prompt_number = ?
|
||||
`).get(prompt.claude_session_id, prompt.prompt_number) as { id: number } | undefined;
|
||||
WHERE content_session_id = ? AND prompt_number = ?
|
||||
`).get(prompt.content_session_id, prompt.prompt_number) as { id: number } | undefined;
|
||||
|
||||
if (existing) {
|
||||
return { imported: false, id: existing.id };
|
||||
@@ -1782,13 +1844,13 @@ export class SessionStore {
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO user_prompts (
|
||||
claude_session_id, prompt_number, prompt_text,
|
||||
content_session_id, prompt_number, prompt_text,
|
||||
created_at, created_at_epoch
|
||||
) VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
prompt.claude_session_id,
|
||||
prompt.content_session_id,
|
||||
prompt.prompt_number,
|
||||
prompt.prompt_text,
|
||||
prompt.created_at,
|
||||
|
||||
@@ -170,8 +170,8 @@ export const migration003: Migration = {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS streaming_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
sdk_session_id TEXT,
|
||||
content_session_id TEXT UNIQUE NOT NULL,
|
||||
memory_session_id TEXT,
|
||||
project TEXT NOT NULL,
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
@@ -185,8 +185,8 @@ export const migration003: Migration = {
|
||||
status TEXT NOT NULL DEFAULT 'active'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_claude_id ON streaming_sessions(claude_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_sdk_id ON streaming_sessions(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_claude_id ON streaming_sessions(content_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_sdk_id ON streaming_sessions(memory_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_project ON streaming_sessions(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_status ON streaming_sessions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_streaming_sessions_started ON streaming_sessions(started_at_epoch DESC);
|
||||
@@ -213,8 +213,8 @@ export const migration004: Migration = {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS sdk_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
sdk_session_id TEXT UNIQUE,
|
||||
content_session_id TEXT UNIQUE NOT NULL,
|
||||
memory_session_id TEXT UNIQUE,
|
||||
project TEXT NOT NULL,
|
||||
user_prompt TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
@@ -224,8 +224,8 @@ export const migration004: Migration = {
|
||||
status TEXT CHECK(status IN ('active', 'completed', 'failed')) NOT NULL DEFAULT 'active'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(claude_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_claude_id ON sdk_sessions(content_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_sdk_id ON sdk_sessions(memory_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_project ON sdk_sessions(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_status ON sdk_sessions(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_started ON sdk_sessions(started_at_epoch DESC);
|
||||
@@ -235,34 +235,34 @@ export const migration004: Migration = {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS observation_queue (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
memory_session_id TEXT NOT NULL,
|
||||
tool_name TEXT NOT NULL,
|
||||
tool_input TEXT NOT NULL,
|
||||
tool_output TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
processed_at_epoch INTEGER,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_observation_queue_sdk_session ON observation_queue(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_observation_queue_sdk_session ON observation_queue(memory_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_observation_queue_processed ON observation_queue(processed_at_epoch);
|
||||
CREATE INDEX IF NOT EXISTS idx_observation_queue_pending ON observation_queue(sdk_session_id, processed_at_epoch);
|
||||
CREATE INDEX IF NOT EXISTS idx_observation_queue_pending ON observation_queue(memory_session_id, processed_at_epoch);
|
||||
`);
|
||||
|
||||
// Observations table - stores extracted observations (what SDK decides is important)
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS observations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
memory_session_id TEXT NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')),
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(memory_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_project ON observations(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_type ON observations(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_epoch DESC);
|
||||
@@ -272,7 +272,7 @@ export const migration004: Migration = {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS session_summaries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT UNIQUE NOT NULL,
|
||||
memory_session_id TEXT UNIQUE NOT NULL,
|
||||
project TEXT NOT NULL,
|
||||
request TEXT,
|
||||
investigated TEXT,
|
||||
@@ -284,10 +284,10 @@ export const migration004: Migration = {
|
||||
notes TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(sdk_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(memory_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_project ON session_summaries(project);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
|
||||
`);
|
||||
@@ -329,8 +329,8 @@ export const migration005: Migration = {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS streaming_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
claude_session_id TEXT UNIQUE NOT NULL,
|
||||
sdk_session_id TEXT,
|
||||
content_session_id TEXT UNIQUE NOT NULL,
|
||||
memory_session_id TEXT,
|
||||
project TEXT NOT NULL,
|
||||
title TEXT,
|
||||
subtitle TEXT,
|
||||
@@ -348,13 +348,13 @@ export const migration005: Migration = {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS observation_queue (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sdk_session_id TEXT NOT NULL,
|
||||
memory_session_id TEXT NOT NULL,
|
||||
tool_name TEXT NOT NULL,
|
||||
tool_input TEXT NOT NULL,
|
||||
tool_output TEXT NOT NULL,
|
||||
created_at_epoch INTEGER NOT NULL,
|
||||
processed_at_epoch INTEGER,
|
||||
FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id) ON DELETE CASCADE
|
||||
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
|
||||
@@ -188,8 +188,8 @@ export function normalizeTimestamp(timestamp: string | Date | number | undefined
|
||||
*/
|
||||
export interface SDKSessionRow {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
sdk_session_id: string | null;
|
||||
content_session_id: string;
|
||||
memory_session_id: string | null;
|
||||
project: string;
|
||||
user_prompt: string | null;
|
||||
started_at: string;
|
||||
@@ -203,7 +203,7 @@ export interface SDKSessionRow {
|
||||
|
||||
export interface ObservationRow {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string;
|
||||
project: string;
|
||||
text: string | null;
|
||||
type: 'decision' | 'bugfix' | 'feature' | 'refactor' | 'discovery' | 'change';
|
||||
@@ -222,7 +222,7 @@ export interface ObservationRow {
|
||||
|
||||
export interface SessionSummaryRow {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string;
|
||||
project: string;
|
||||
request: string | null;
|
||||
investigated: string | null;
|
||||
@@ -240,7 +240,7 @@ export interface SessionSummaryRow {
|
||||
|
||||
export interface UserPromptRow {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
content_session_id: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
created_at: string;
|
||||
|
||||
@@ -26,7 +26,7 @@ interface ChromaDocument {
|
||||
|
||||
interface StoredObservation {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string;
|
||||
project: string;
|
||||
text: string | null;
|
||||
type: string;
|
||||
@@ -45,7 +45,7 @@ interface StoredObservation {
|
||||
|
||||
interface StoredSummary {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string;
|
||||
project: string;
|
||||
request: string | null;
|
||||
investigated: string | null;
|
||||
@@ -61,12 +61,12 @@ interface StoredSummary {
|
||||
|
||||
interface StoredUserPrompt {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
content_session_id: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string;
|
||||
project: string;
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ export class ChromaSync {
|
||||
const baseMetadata: Record<string, string | number> = {
|
||||
sqlite_id: obs.id,
|
||||
doc_type: 'observation',
|
||||
sdk_session_id: obs.sdk_session_id,
|
||||
memory_session_id: obs.memory_session_id,
|
||||
project: obs.project,
|
||||
created_at_epoch: obs.created_at_epoch,
|
||||
type: obs.type || 'discovery',
|
||||
@@ -262,7 +262,7 @@ export class ChromaSync {
|
||||
const baseMetadata: Record<string, string | number> = {
|
||||
sqlite_id: summary.id,
|
||||
doc_type: 'session_summary',
|
||||
sdk_session_id: summary.sdk_session_id,
|
||||
memory_session_id: summary.memory_session_id,
|
||||
project: summary.project,
|
||||
created_at_epoch: summary.created_at_epoch,
|
||||
prompt_number: summary.prompt_number || 0
|
||||
@@ -368,7 +368,7 @@ export class ChromaSync {
|
||||
*/
|
||||
async syncObservation(
|
||||
observationId: number,
|
||||
sdkSessionId: string,
|
||||
memorySessionId: string,
|
||||
project: string,
|
||||
obs: ParsedObservation,
|
||||
promptNumber: number,
|
||||
@@ -378,7 +378,7 @@ export class ChromaSync {
|
||||
// Convert ParsedObservation to StoredObservation format
|
||||
const stored: StoredObservation = {
|
||||
id: observationId,
|
||||
sdk_session_id: sdkSessionId,
|
||||
memory_session_id: memorySessionId,
|
||||
project: project,
|
||||
text: null, // Legacy field, not used
|
||||
type: obs.type,
|
||||
@@ -412,7 +412,7 @@ export class ChromaSync {
|
||||
*/
|
||||
async syncSummary(
|
||||
summaryId: number,
|
||||
sdkSessionId: string,
|
||||
memorySessionId: string,
|
||||
project: string,
|
||||
summary: ParsedSummary,
|
||||
promptNumber: number,
|
||||
@@ -422,7 +422,7 @@ export class ChromaSync {
|
||||
// Convert ParsedSummary to StoredSummary format
|
||||
const stored: StoredSummary = {
|
||||
id: summaryId,
|
||||
sdk_session_id: sdkSessionId,
|
||||
memory_session_id: memorySessionId,
|
||||
project: project,
|
||||
request: summary.request,
|
||||
investigated: summary.investigated,
|
||||
@@ -458,7 +458,7 @@ export class ChromaSync {
|
||||
metadata: {
|
||||
sqlite_id: prompt.id,
|
||||
doc_type: 'user_prompt',
|
||||
sdk_session_id: prompt.sdk_session_id,
|
||||
memory_session_id: prompt.memory_session_id,
|
||||
project: prompt.project,
|
||||
created_at_epoch: prompt.created_at_epoch,
|
||||
prompt_number: prompt.prompt_number
|
||||
@@ -472,7 +472,7 @@ export class ChromaSync {
|
||||
*/
|
||||
async syncUserPrompt(
|
||||
promptId: number,
|
||||
sdkSessionId: string,
|
||||
memorySessionId: string,
|
||||
project: string,
|
||||
promptText: string,
|
||||
promptNumber: number,
|
||||
@@ -481,12 +481,12 @@ export class ChromaSync {
|
||||
// Create StoredUserPrompt format
|
||||
const stored: StoredUserPrompt = {
|
||||
id: promptId,
|
||||
claude_session_id: '', // Not needed for Chroma sync
|
||||
content_session_id: '', // Not needed for Chroma sync
|
||||
prompt_number: promptNumber,
|
||||
prompt_text: promptText,
|
||||
created_at: new Date(createdAtEpoch * 1000).toISOString(),
|
||||
created_at_epoch: createdAtEpoch,
|
||||
sdk_session_id: sdkSessionId,
|
||||
memory_session_id: memorySessionId,
|
||||
project: project
|
||||
};
|
||||
|
||||
@@ -697,9 +697,9 @@ export class ChromaSync {
|
||||
SELECT
|
||||
up.*,
|
||||
s.project,
|
||||
s.sdk_session_id
|
||||
s.memory_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
|
||||
WHERE s.project = ? ${promptExclusionClause}
|
||||
ORDER BY up.id ASC
|
||||
`).all(this.project) as StoredUserPrompt[];
|
||||
@@ -707,7 +707,7 @@ export class ChromaSync {
|
||||
const totalPromptCount = db.db.prepare(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
|
||||
WHERE s.project = ?
|
||||
`).get(this.project) as { count: number };
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ export interface ConversationMessage {
|
||||
|
||||
export interface ActiveSession {
|
||||
sessionDbId: number;
|
||||
claudeSessionId: string;
|
||||
sdkSessionId: string | null;
|
||||
contentSessionId: string; // User's Claude Code session being observed
|
||||
memorySessionId: string | null; // Memory agent's session ID for resume
|
||||
project: string;
|
||||
userPrompt: string;
|
||||
pendingMessages: PendingMessage[]; // Deprecated: now using persistent store, kept for compatibility
|
||||
@@ -110,7 +110,7 @@ export interface ViewerSettings {
|
||||
|
||||
export interface Observation {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string; // Renamed from sdk_session_id
|
||||
project: string;
|
||||
type: string;
|
||||
title: string;
|
||||
@@ -128,7 +128,7 @@ export interface Observation {
|
||||
|
||||
export interface Summary {
|
||||
id: number;
|
||||
session_id: string; // claude_session_id (from JOIN)
|
||||
session_id: string; // content_session_id (from JOIN)
|
||||
project: string;
|
||||
request: string | null;
|
||||
investigated: string | null;
|
||||
@@ -142,7 +142,7 @@ export interface Summary {
|
||||
|
||||
export interface UserPrompt {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
content_session_id: string; // Renamed from claude_session_id
|
||||
project: string; // From JOIN with sdk_sessions
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
@@ -152,10 +152,10 @@ export interface UserPrompt {
|
||||
|
||||
export interface DBSession {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
content_session_id: string; // Renamed from claude_session_id
|
||||
project: string;
|
||||
user_prompt: string;
|
||||
sdk_session_id: string | null;
|
||||
memory_session_id: string | null; // Renamed from sdk_session_id
|
||||
status: 'active' | 'completed' | 'failed';
|
||||
started_at: string;
|
||||
started_at_epoch: number;
|
||||
|
||||
@@ -93,8 +93,8 @@ export class DatabaseManager {
|
||||
*/
|
||||
getSessionById(sessionDbId: number): {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
sdk_session_id: string | null;
|
||||
content_session_id: string;
|
||||
memory_session_id: string | null;
|
||||
project: string;
|
||||
user_prompt: string;
|
||||
} {
|
||||
|
||||
@@ -152,8 +152,8 @@ export class GeminiAgent {
|
||||
|
||||
// Build initial prompt
|
||||
const initPrompt = session.lastPromptNumber === 1
|
||||
? buildInitPrompt(session.project, session.claudeSessionId, session.userPrompt, mode)
|
||||
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.claudeSessionId, mode);
|
||||
? buildInitPrompt(session.project, session.contentSessionId, session.userPrompt, mode)
|
||||
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.contentSessionId, mode);
|
||||
|
||||
// Add to conversation history and query Gemini with full context
|
||||
session.conversationHistory.push({ role: 'user', content: initPrompt });
|
||||
@@ -224,7 +224,7 @@ export class GeminiAgent {
|
||||
// Build summary prompt
|
||||
const summaryPrompt = buildSummaryPrompt({
|
||||
id: session.sessionDbId,
|
||||
sdk_session_id: session.sdkSessionId,
|
||||
memory_session_id: session.memorySessionId,
|
||||
project: session.project,
|
||||
user_prompt: session.userPrompt,
|
||||
last_user_message: message.last_user_message || '',
|
||||
@@ -374,12 +374,12 @@ export class GeminiAgent {
|
||||
originalTimestamp: number | null
|
||||
): Promise<void> {
|
||||
// Parse observations (same XML format)
|
||||
const observations = parseObservations(text, session.claudeSessionId);
|
||||
const observations = parseObservations(text, session.contentSessionId);
|
||||
|
||||
// Store observations with original timestamp (if processing backlog) or current time
|
||||
for (const obs of observations) {
|
||||
const { id: obsId, createdAtEpoch } = this.dbManager.getSessionStore().storeObservation(
|
||||
session.claudeSessionId,
|
||||
session.contentSessionId,
|
||||
session.project,
|
||||
obs,
|
||||
session.lastPromptNumber,
|
||||
@@ -397,7 +397,7 @@ export class GeminiAgent {
|
||||
// Sync to Chroma
|
||||
this.dbManager.getChromaSync().syncObservation(
|
||||
obsId,
|
||||
session.claudeSessionId,
|
||||
session.contentSessionId,
|
||||
session.project,
|
||||
obs,
|
||||
session.lastPromptNumber,
|
||||
@@ -413,8 +413,8 @@ export class GeminiAgent {
|
||||
type: 'new_observation',
|
||||
observation: {
|
||||
id: obsId,
|
||||
sdk_session_id: session.sdkSessionId,
|
||||
session_id: session.claudeSessionId,
|
||||
memory_session_id: session.memorySessionId,
|
||||
session_id: session.contentSessionId,
|
||||
type: obs.type,
|
||||
title: obs.title,
|
||||
subtitle: obs.subtitle,
|
||||
@@ -447,7 +447,7 @@ export class GeminiAgent {
|
||||
};
|
||||
|
||||
const { id: summaryId, createdAtEpoch } = this.dbManager.getSessionStore().storeSummary(
|
||||
session.claudeSessionId,
|
||||
session.contentSessionId,
|
||||
session.project,
|
||||
summaryForStore,
|
||||
session.lastPromptNumber,
|
||||
@@ -464,7 +464,7 @@ export class GeminiAgent {
|
||||
// Sync to Chroma
|
||||
this.dbManager.getChromaSync().syncSummary(
|
||||
summaryId,
|
||||
session.claudeSessionId,
|
||||
session.contentSessionId,
|
||||
session.project,
|
||||
summaryForStore,
|
||||
session.lastPromptNumber,
|
||||
@@ -480,7 +480,7 @@ export class GeminiAgent {
|
||||
type: 'new_summary',
|
||||
summary: {
|
||||
id: summaryId,
|
||||
session_id: session.claudeSessionId,
|
||||
session_id: session.contentSessionId,
|
||||
request: summary.request,
|
||||
investigated: summary.investigated,
|
||||
learned: summary.learned,
|
||||
|
||||
@@ -112,8 +112,8 @@ export class OpenRouterAgent {
|
||||
|
||||
// Build initial prompt
|
||||
const initPrompt = session.lastPromptNumber === 1
|
||||
? buildInitPrompt(session.project, session.claudeSessionId, session.userPrompt, mode)
|
||||
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.claudeSessionId, mode);
|
||||
? buildInitPrompt(session.project, session.contentSessionId, session.userPrompt, mode)
|
||||
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.contentSessionId, mode);
|
||||
|
||||
// Add to conversation history and query OpenRouter with full context
|
||||
session.conversationHistory.push({ role: 'user', content: initPrompt });
|
||||
@@ -183,7 +183,7 @@ export class OpenRouterAgent {
|
||||
// Build summary prompt
|
||||
const summaryPrompt = buildSummaryPrompt({
|
||||
id: session.sessionDbId,
|
||||
sdk_session_id: session.sdkSessionId,
|
||||
memory_session_id: session.memorySessionId,
|
||||
project: session.project,
|
||||
user_prompt: session.userPrompt,
|
||||
last_user_message: message.last_user_message || '',
|
||||
@@ -417,12 +417,12 @@ export class OpenRouterAgent {
|
||||
originalTimestamp: number | null
|
||||
): Promise<void> {
|
||||
// Parse observations (same XML format)
|
||||
const observations = parseObservations(text, session.claudeSessionId);
|
||||
const observations = parseObservations(text, session.contentSessionId);
|
||||
|
||||
// Store observations with original timestamp (if processing backlog) or current time
|
||||
for (const obs of observations) {
|
||||
const { id: obsId, createdAtEpoch } = this.dbManager.getSessionStore().storeObservation(
|
||||
session.claudeSessionId,
|
||||
session.contentSessionId,
|
||||
session.project,
|
||||
obs,
|
||||
session.lastPromptNumber,
|
||||
@@ -440,7 +440,7 @@ export class OpenRouterAgent {
|
||||
// Sync to Chroma
|
||||
this.dbManager.getChromaSync().syncObservation(
|
||||
obsId,
|
||||
session.claudeSessionId,
|
||||
session.contentSessionId,
|
||||
session.project,
|
||||
obs,
|
||||
session.lastPromptNumber,
|
||||
@@ -456,8 +456,8 @@ export class OpenRouterAgent {
|
||||
type: 'new_observation',
|
||||
observation: {
|
||||
id: obsId,
|
||||
sdk_session_id: session.sdkSessionId,
|
||||
session_id: session.claudeSessionId,
|
||||
memory_session_id: session.memorySessionId,
|
||||
session_id: session.contentSessionId,
|
||||
type: obs.type,
|
||||
title: obs.title,
|
||||
subtitle: obs.subtitle,
|
||||
@@ -490,7 +490,7 @@ export class OpenRouterAgent {
|
||||
};
|
||||
|
||||
const { id: summaryId, createdAtEpoch } = this.dbManager.getSessionStore().storeSummary(
|
||||
session.claudeSessionId,
|
||||
session.contentSessionId,
|
||||
session.project,
|
||||
summaryForStore,
|
||||
session.lastPromptNumber,
|
||||
@@ -507,7 +507,7 @@ export class OpenRouterAgent {
|
||||
// Sync to Chroma
|
||||
this.dbManager.getChromaSync().syncSummary(
|
||||
summaryId,
|
||||
session.claudeSessionId,
|
||||
session.contentSessionId,
|
||||
session.project,
|
||||
summaryForStore,
|
||||
session.lastPromptNumber,
|
||||
@@ -523,7 +523,7 @@ export class OpenRouterAgent {
|
||||
type: 'new_summary',
|
||||
summary: {
|
||||
id: summaryId,
|
||||
session_id: session.claudeSessionId,
|
||||
session_id: session.contentSessionId,
|
||||
request: summary.request,
|
||||
investigated: summary.investigated,
|
||||
learned: summary.learned,
|
||||
|
||||
@@ -74,7 +74,7 @@ export class PaginationHelper {
|
||||
getObservations(offset: number, limit: number, project?: string): PaginatedResult<Observation> {
|
||||
const result = this.paginate<Observation>(
|
||||
'observations',
|
||||
'id, sdk_session_id, project, type, title, subtitle, narrative, text, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch',
|
||||
'id, memory_session_id, project, type, title, subtitle, narrative, text, facts, concepts, files_read, files_modified, prompt_number, created_at, created_at_epoch',
|
||||
offset,
|
||||
limit,
|
||||
project
|
||||
@@ -96,7 +96,7 @@ export class PaginationHelper {
|
||||
let query = `
|
||||
SELECT
|
||||
ss.id,
|
||||
s.claude_session_id as session_id,
|
||||
s.content_session_id as session_id,
|
||||
ss.request,
|
||||
ss.investigated,
|
||||
ss.learned,
|
||||
@@ -106,7 +106,7 @@ export class PaginationHelper {
|
||||
ss.created_at,
|
||||
ss.created_at_epoch
|
||||
FROM session_summaries ss
|
||||
JOIN sdk_sessions s ON ss.sdk_session_id = s.sdk_session_id
|
||||
JOIN sdk_sessions s ON ss.memory_session_id = s.memory_session_id
|
||||
`;
|
||||
const params: any[] = [];
|
||||
|
||||
@@ -136,9 +136,9 @@ export class PaginationHelper {
|
||||
const db = this.dbManager.getSessionStore().db;
|
||||
|
||||
let query = `
|
||||
SELECT up.id, up.claude_session_id, s.project, up.prompt_number, up.prompt_text, up.created_at, up.created_at_epoch
|
||||
SELECT up.id, up.content_session_id, s.project, up.prompt_number, up.prompt_text, up.created_at, up.created_at_epoch
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
|
||||
`;
|
||||
const params: any[] = [];
|
||||
|
||||
|
||||
@@ -66,17 +66,20 @@ export class SDKAgent {
|
||||
|
||||
logger.info('SDK', 'Starting SDK query', {
|
||||
sessionDbId: session.sessionDbId,
|
||||
claudeSessionId: session.claudeSessionId,
|
||||
resume_parameter: session.claudeSessionId,
|
||||
contentSessionId: session.contentSessionId,
|
||||
memorySessionId: session.memorySessionId,
|
||||
resume_parameter: session.memorySessionId || '(none - fresh start)',
|
||||
lastPromptNumber: session.lastPromptNumber
|
||||
});
|
||||
|
||||
// Run Agent SDK query loop
|
||||
// Use memorySessionId for resume (captured from previous SDK response) if available
|
||||
const queryResult = query({
|
||||
prompt: messageGenerator,
|
||||
options: {
|
||||
model: modelId,
|
||||
resume: session.claudeSessionId,
|
||||
// Only resume if we have a captured memory session ID from previous SDK interaction
|
||||
...(session.memorySessionId && { resume: session.memorySessionId }),
|
||||
disallowedTools,
|
||||
abortController: session.abortController,
|
||||
pathToClaudeCodeExecutable: claudePath
|
||||
@@ -85,6 +88,21 @@ export class SDKAgent {
|
||||
|
||||
// Process SDK messages
|
||||
for await (const message of queryResult) {
|
||||
// Capture memory session ID from first SDK message (any type has session_id)
|
||||
// This enables resume for subsequent generator starts within the same user session
|
||||
if (!session.memorySessionId && message.session_id) {
|
||||
session.memorySessionId = message.session_id;
|
||||
// Persist to database for cross-restart recovery
|
||||
this.dbManager.getSessionStore().updateMemorySessionId(
|
||||
session.sessionDbId,
|
||||
message.session_id
|
||||
);
|
||||
logger.info('SDK', 'Captured memory session ID', {
|
||||
sessionDbId: session.sessionDbId,
|
||||
memorySessionId: message.session_id
|
||||
});
|
||||
}
|
||||
|
||||
// Handle assistant messages
|
||||
if (message.type === 'assistant') {
|
||||
const content = message.message.content;
|
||||
@@ -184,7 +202,7 @@ export class SDKAgent {
|
||||
* - Continuation prompt for same session
|
||||
* - Includes session context and prompt number
|
||||
*
|
||||
* BOTH prompts receive session.claudeSessionId:
|
||||
* BOTH prompts receive session.contentSessionId:
|
||||
* - This comes from the hook's session_id (see new-hook.ts)
|
||||
* - Same session_id used by SAVE hook to store observations
|
||||
* - This is how everything stays connected in one unified session
|
||||
@@ -207,28 +225,28 @@ export class SDKAgent {
|
||||
const isInitPrompt = session.lastPromptNumber === 1;
|
||||
logger.info('SDK', 'Creating message generator', {
|
||||
sessionDbId: session.sessionDbId,
|
||||
claudeSessionId: session.claudeSessionId,
|
||||
contentSessionId: session.contentSessionId,
|
||||
lastPromptNumber: session.lastPromptNumber,
|
||||
isInitPrompt,
|
||||
promptType: isInitPrompt ? 'INIT' : 'CONTINUATION'
|
||||
});
|
||||
|
||||
const initPrompt = isInitPrompt
|
||||
? buildInitPrompt(session.project, session.claudeSessionId, session.userPrompt, mode)
|
||||
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.claudeSessionId, mode);
|
||||
? buildInitPrompt(session.project, session.contentSessionId, session.userPrompt, mode)
|
||||
: buildContinuationPrompt(session.userPrompt, session.lastPromptNumber, session.contentSessionId, mode);
|
||||
|
||||
// Add to shared conversation history for provider interop
|
||||
session.conversationHistory.push({ role: 'user', content: initPrompt });
|
||||
|
||||
// Yield initial user prompt with context (or continuation if prompt #2+)
|
||||
// CRITICAL: Both paths use session.claudeSessionId from the hook
|
||||
// CRITICAL: Both paths use session.contentSessionId from the hook
|
||||
yield {
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: initPrompt
|
||||
},
|
||||
session_id: session.claudeSessionId,
|
||||
session_id: session.contentSessionId,
|
||||
parent_tool_use_id: null,
|
||||
isSynthetic: true
|
||||
};
|
||||
@@ -259,14 +277,14 @@ export class SDKAgent {
|
||||
role: 'user',
|
||||
content: obsPrompt
|
||||
},
|
||||
session_id: session.claudeSessionId,
|
||||
session_id: session.contentSessionId,
|
||||
parent_tool_use_id: null,
|
||||
isSynthetic: true
|
||||
};
|
||||
} else if (message.type === 'summarize') {
|
||||
const summaryPrompt = buildSummaryPrompt({
|
||||
id: session.sessionDbId,
|
||||
sdk_session_id: session.sdkSessionId,
|
||||
memory_session_id: session.memorySessionId,
|
||||
project: session.project,
|
||||
user_prompt: session.userPrompt,
|
||||
last_user_message: message.last_user_message || '',
|
||||
@@ -282,7 +300,7 @@ export class SDKAgent {
|
||||
role: 'user',
|
||||
content: summaryPrompt
|
||||
},
|
||||
session_id: session.claudeSessionId,
|
||||
session_id: session.contentSessionId,
|
||||
parent_tool_use_id: null,
|
||||
isSynthetic: true
|
||||
};
|
||||
@@ -305,12 +323,12 @@ export class SDKAgent {
|
||||
}
|
||||
|
||||
// Parse observations
|
||||
const observations = parseObservations(text, session.claudeSessionId);
|
||||
const observations = parseObservations(text, session.contentSessionId);
|
||||
|
||||
// Store observations with original timestamp (if processing backlog) or current time
|
||||
for (const obs of observations) {
|
||||
const { id: obsId, createdAtEpoch } = this.dbManager.getSessionStore().storeObservation(
|
||||
session.claudeSessionId,
|
||||
session.contentSessionId,
|
||||
session.project,
|
||||
obs,
|
||||
session.lastPromptNumber,
|
||||
@@ -335,7 +353,7 @@ export class SDKAgent {
|
||||
const obsTitle = obs.title || '(untitled)';
|
||||
this.dbManager.getChromaSync().syncObservation(
|
||||
obsId,
|
||||
session.claudeSessionId,
|
||||
session.contentSessionId,
|
||||
session.project,
|
||||
obs,
|
||||
session.lastPromptNumber,
|
||||
@@ -363,8 +381,8 @@ export class SDKAgent {
|
||||
type: 'new_observation',
|
||||
observation: {
|
||||
id: obsId,
|
||||
sdk_session_id: session.sdkSessionId,
|
||||
session_id: session.claudeSessionId,
|
||||
memory_session_id: session.memorySessionId,
|
||||
session_id: session.contentSessionId,
|
||||
type: obs.type,
|
||||
title: obs.title,
|
||||
subtitle: obs.subtitle,
|
||||
@@ -388,7 +406,7 @@ export class SDKAgent {
|
||||
// Store summary with original timestamp (if processing backlog) or current time
|
||||
if (summary) {
|
||||
const { id: summaryId, createdAtEpoch } = this.dbManager.getSessionStore().storeSummary(
|
||||
session.claudeSessionId,
|
||||
session.contentSessionId,
|
||||
session.project,
|
||||
summary,
|
||||
session.lastPromptNumber,
|
||||
@@ -410,7 +428,7 @@ export class SDKAgent {
|
||||
const summaryRequest = summary.request || '(no request)';
|
||||
this.dbManager.getChromaSync().syncSummary(
|
||||
summaryId,
|
||||
session.claudeSessionId,
|
||||
session.contentSessionId,
|
||||
session.project,
|
||||
summary,
|
||||
session.lastPromptNumber,
|
||||
@@ -436,7 +454,7 @@ export class SDKAgent {
|
||||
type: 'new_summary',
|
||||
summary: {
|
||||
id: summaryId,
|
||||
session_id: session.claudeSessionId,
|
||||
session_id: session.contentSessionId,
|
||||
request: summary.request,
|
||||
investigated: summary.investigated,
|
||||
learned: summary.learned,
|
||||
|
||||
@@ -59,7 +59,7 @@ export class SessionManager {
|
||||
if (session) {
|
||||
logger.info('SESSION', 'Returning cached session', {
|
||||
sessionDbId,
|
||||
claudeSessionId: session.claudeSessionId,
|
||||
contentSessionId: session.contentSessionId,
|
||||
lastPromptNumber: session.lastPromptNumber
|
||||
});
|
||||
|
||||
@@ -101,8 +101,8 @@ export class SessionManager {
|
||||
|
||||
logger.info('SESSION', 'Fetched session from database', {
|
||||
sessionDbId,
|
||||
claude_session_id: dbSession.claude_session_id,
|
||||
sdk_session_id: dbSession.sdk_session_id
|
||||
content_session_id: dbSession.content_session_id,
|
||||
memory_session_id: dbSession.memory_session_id
|
||||
});
|
||||
|
||||
// Use currentUserPrompt if provided, otherwise fall back to database (first prompt)
|
||||
@@ -123,16 +123,17 @@ export class SessionManager {
|
||||
}
|
||||
|
||||
// Create active session
|
||||
// Load memorySessionId from database if previously captured (enables resume across restarts)
|
||||
session = {
|
||||
sessionDbId,
|
||||
claudeSessionId: dbSession.claude_session_id,
|
||||
sdkSessionId: null,
|
||||
contentSessionId: dbSession.content_session_id,
|
||||
memorySessionId: dbSession.memory_session_id || null,
|
||||
project: dbSession.project,
|
||||
userPrompt,
|
||||
pendingMessages: [],
|
||||
abortController: new AbortController(),
|
||||
generatorPromise: null,
|
||||
lastPromptNumber: promptNumber || this.dbManager.getSessionStore().getPromptNumberFromUserPrompts(dbSession.claude_session_id),
|
||||
lastPromptNumber: promptNumber || this.dbManager.getSessionStore().getPromptNumberFromUserPrompts(dbSession.content_session_id),
|
||||
startTime: Date.now(),
|
||||
cumulativeInputTokens: 0,
|
||||
cumulativeOutputTokens: 0,
|
||||
@@ -144,8 +145,9 @@ export class SessionManager {
|
||||
|
||||
logger.info('SESSION', 'Creating new session object', {
|
||||
sessionDbId,
|
||||
claudeSessionId: dbSession.claude_session_id,
|
||||
lastPromptNumber: promptNumber || this.dbManager.getSessionStore().getPromptNumberFromUserPrompts(dbSession.claude_session_id)
|
||||
contentSessionId: dbSession.content_session_id,
|
||||
memorySessionId: dbSession.memory_session_id || '(none - fresh session)',
|
||||
lastPromptNumber: promptNumber || this.dbManager.getSessionStore().getPromptNumberFromUserPrompts(dbSession.content_session_id)
|
||||
});
|
||||
|
||||
this.sessions.set(sessionDbId, session);
|
||||
@@ -157,7 +159,7 @@ export class SessionManager {
|
||||
logger.info('SESSION', 'Session initialized', {
|
||||
sessionId: sessionDbId,
|
||||
project: session.project,
|
||||
claudeSessionId: session.claudeSessionId,
|
||||
contentSessionId: session.contentSessionId,
|
||||
queueDepth: 0,
|
||||
hasGenerator: false
|
||||
});
|
||||
@@ -197,7 +199,7 @@ export class SessionManager {
|
||||
};
|
||||
|
||||
try {
|
||||
const messageId = this.getPendingStore().enqueue(sessionDbId, session.claudeSessionId, message);
|
||||
const messageId = this.getPendingStore().enqueue(sessionDbId, session.contentSessionId, message);
|
||||
logger.debug('SESSION', `Observation persisted to DB`, {
|
||||
sessionId: sessionDbId,
|
||||
messageId,
|
||||
@@ -247,7 +249,7 @@ export class SessionManager {
|
||||
};
|
||||
|
||||
try {
|
||||
const messageId = this.getPendingStore().enqueue(sessionDbId, session.claudeSessionId, message);
|
||||
const messageId = this.getPendingStore().enqueue(sessionDbId, session.contentSessionId, message);
|
||||
logger.debug('SESSION', `Summarize persisted to DB`, {
|
||||
sessionId: sessionDbId,
|
||||
messageId
|
||||
|
||||
@@ -21,7 +21,7 @@ export class SessionEventBroadcaster {
|
||||
*/
|
||||
broadcastNewPrompt(prompt: {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
content_session_id: string;
|
||||
project: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
|
||||
@@ -158,18 +158,18 @@ export class DataRoutes extends BaseRouteHandler {
|
||||
/**
|
||||
* Get SDK sessions by SDK session IDs
|
||||
* POST /api/sdk-sessions/batch
|
||||
* Body: { sdkSessionIds: string[] }
|
||||
* Body: { memorySessionIds: string[] }
|
||||
*/
|
||||
private handleGetSdkSessionsByIds = this.wrapHandler((req: Request, res: Response): void => {
|
||||
const { sdkSessionIds } = req.body;
|
||||
const { memorySessionIds } = req.body;
|
||||
|
||||
if (!Array.isArray(sdkSessionIds)) {
|
||||
this.badRequest(res, 'sdkSessionIds must be an array');
|
||||
if (!Array.isArray(memorySessionIds)) {
|
||||
this.badRequest(res, 'memorySessionIds must be an array');
|
||||
return;
|
||||
}
|
||||
|
||||
const store = this.dbManager.getSessionStore();
|
||||
const sessions = store.getSdkSessionsBySessionIds(sdkSessionIds);
|
||||
const sessions = store.getSdkSessionsBySessionIds(memorySessionIds);
|
||||
res.json(sessions);
|
||||
});
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
* Get the appropriate agent based on settings
|
||||
* Throws error if provider is selected but not configured (no silent fallback)
|
||||
*
|
||||
* Note: Session linking via claudeSessionId allows provider switching mid-session.
|
||||
* Note: Session linking via contentSessionId allows provider switching mid-session.
|
||||
* The conversationHistory on ActiveSession maintains context across providers.
|
||||
*/
|
||||
private getActiveAgent(): SDKAgent | GeminiAgent | OpenRouterAgent {
|
||||
@@ -217,7 +217,7 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
app.delete('/sessions/:sessionDbId', this.handleSessionDelete.bind(this));
|
||||
app.post('/sessions/:sessionDbId/complete', this.handleSessionComplete.bind(this));
|
||||
|
||||
// New session endpoints (use claudeSessionId)
|
||||
// New session endpoints (use contentSessionId)
|
||||
app.post('/api/sessions/init', this.handleSessionInitByClaudeId.bind(this));
|
||||
app.post('/api/sessions/observations', this.handleObservationsByClaudeId.bind(this));
|
||||
app.post('/api/sessions/summarize', this.handleSummarizeByClaudeId.bind(this));
|
||||
@@ -240,13 +240,13 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
const session = this.sessionManager.initializeSession(sessionDbId, userPrompt, promptNumber);
|
||||
|
||||
// Get the latest user_prompt for this session to sync to Chroma
|
||||
const latestPrompt = this.dbManager.getSessionStore().getLatestUserPrompt(session.claudeSessionId);
|
||||
const latestPrompt = this.dbManager.getSessionStore().getLatestUserPrompt(session.contentSessionId);
|
||||
|
||||
// Broadcast new prompt to SSE clients (for web UI)
|
||||
if (latestPrompt) {
|
||||
this.eventBroadcaster.broadcastNewPrompt({
|
||||
id: latestPrompt.id,
|
||||
claude_session_id: latestPrompt.claude_session_id,
|
||||
content_session_id: latestPrompt.content_session_id,
|
||||
project: latestPrompt.project,
|
||||
prompt_number: latestPrompt.prompt_number,
|
||||
prompt_text: latestPrompt.prompt_text,
|
||||
@@ -258,7 +258,7 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
const promptText = latestPrompt.prompt_text;
|
||||
this.dbManager.getChromaSync().syncUserPrompt(
|
||||
latestPrompt.id,
|
||||
latestPrompt.sdk_session_id,
|
||||
latestPrompt.memory_session_id,
|
||||
latestPrompt.project,
|
||||
promptText,
|
||||
latestPrompt.prompt_number,
|
||||
@@ -387,15 +387,15 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
});
|
||||
|
||||
/**
|
||||
* Queue observations by claudeSessionId (post-tool-use-hook uses this)
|
||||
* Queue observations by contentSessionId (post-tool-use-hook uses this)
|
||||
* POST /api/sessions/observations
|
||||
* Body: { claudeSessionId, tool_name, tool_input, tool_response, cwd }
|
||||
* Body: { contentSessionId, tool_name, tool_input, tool_response, cwd }
|
||||
*/
|
||||
private handleObservationsByClaudeId = this.wrapHandler((req: Request, res: Response): void => {
|
||||
const { claudeSessionId, tool_name, tool_input, tool_response, cwd } = req.body;
|
||||
const { contentSessionId, tool_name, tool_input, tool_response, cwd } = req.body;
|
||||
|
||||
if (!claudeSessionId) {
|
||||
return this.badRequest(res, 'Missing claudeSessionId');
|
||||
if (!contentSessionId) {
|
||||
return this.badRequest(res, 'Missing contentSessionId');
|
||||
}
|
||||
|
||||
// Load skip tools from settings
|
||||
@@ -426,13 +426,13 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
const store = this.dbManager.getSessionStore();
|
||||
|
||||
// Get or create session
|
||||
const sessionDbId = store.createSDKSession(claudeSessionId, '', '');
|
||||
const promptNumber = store.getPromptNumberFromUserPrompts(claudeSessionId);
|
||||
const sessionDbId = store.createSDKSession(contentSessionId, '', '');
|
||||
const promptNumber = store.getPromptNumberFromUserPrompts(contentSessionId);
|
||||
|
||||
// Privacy check: skip if user prompt was entirely private
|
||||
const userPrompt = PrivacyCheckValidator.checkUserPromptPrivacy(
|
||||
store,
|
||||
claudeSessionId,
|
||||
contentSessionId,
|
||||
promptNumber,
|
||||
'observation',
|
||||
sessionDbId,
|
||||
@@ -477,29 +477,29 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
});
|
||||
|
||||
/**
|
||||
* Queue summarize by claudeSessionId (summary-hook uses this)
|
||||
* Queue summarize by contentSessionId (summary-hook uses this)
|
||||
* POST /api/sessions/summarize
|
||||
* Body: { claudeSessionId, last_user_message, last_assistant_message }
|
||||
* Body: { contentSessionId, last_user_message, last_assistant_message }
|
||||
*
|
||||
* Checks privacy, queues summarize request for SDK agent
|
||||
*/
|
||||
private handleSummarizeByClaudeId = this.wrapHandler((req: Request, res: Response): void => {
|
||||
const { claudeSessionId, last_user_message, last_assistant_message } = req.body;
|
||||
const { contentSessionId, last_user_message, last_assistant_message } = req.body;
|
||||
|
||||
if (!claudeSessionId) {
|
||||
return this.badRequest(res, 'Missing claudeSessionId');
|
||||
if (!contentSessionId) {
|
||||
return this.badRequest(res, 'Missing contentSessionId');
|
||||
}
|
||||
|
||||
const store = this.dbManager.getSessionStore();
|
||||
|
||||
// Get or create session
|
||||
const sessionDbId = store.createSDKSession(claudeSessionId, '', '');
|
||||
const promptNumber = store.getPromptNumberFromUserPrompts(claudeSessionId);
|
||||
const sessionDbId = store.createSDKSession(contentSessionId, '', '');
|
||||
const promptNumber = store.getPromptNumberFromUserPrompts(contentSessionId);
|
||||
|
||||
// Privacy check: skip if user prompt was entirely private
|
||||
const userPrompt = PrivacyCheckValidator.checkUserPromptPrivacy(
|
||||
store,
|
||||
claudeSessionId,
|
||||
contentSessionId,
|
||||
promptNumber,
|
||||
'summarize',
|
||||
sessionDbId
|
||||
@@ -532,9 +532,9 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize session by claudeSessionId (new-hook uses this)
|
||||
* Initialize session by contentSessionId (new-hook uses this)
|
||||
* POST /api/sessions/init
|
||||
* Body: { claudeSessionId, project, prompt }
|
||||
* Body: { contentSessionId, project, prompt }
|
||||
*
|
||||
* Performs all session initialization DB operations:
|
||||
* - Creates/gets SDK session (idempotent)
|
||||
@@ -544,31 +544,31 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
* Returns: { sessionDbId, promptNumber, skipped: boolean, reason?: string }
|
||||
*/
|
||||
private handleSessionInitByClaudeId = this.wrapHandler((req: Request, res: Response): void => {
|
||||
const { claudeSessionId, project, prompt } = req.body;
|
||||
const { contentSessionId, project, prompt } = req.body;
|
||||
|
||||
logger.info('HTTP', 'SessionRoutes: handleSessionInitByClaudeId called', {
|
||||
claudeSessionId,
|
||||
contentSessionId,
|
||||
project,
|
||||
prompt_length: prompt?.length
|
||||
});
|
||||
|
||||
// Validate required parameters
|
||||
if (!this.validateRequired(req, res, ['claudeSessionId', 'project', 'prompt'])) {
|
||||
if (!this.validateRequired(req, res, ['contentSessionId', 'project', 'prompt'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
const store = this.dbManager.getSessionStore();
|
||||
|
||||
// Step 1: Create/get SDK session (idempotent INSERT OR IGNORE)
|
||||
const sessionDbId = store.createSDKSession(claudeSessionId, project, prompt);
|
||||
const sessionDbId = store.createSDKSession(contentSessionId, project, prompt);
|
||||
|
||||
logger.info('HTTP', 'SessionRoutes: createSDKSession returned', {
|
||||
sessionDbId,
|
||||
claudeSessionId
|
||||
contentSessionId
|
||||
});
|
||||
|
||||
// Step 2: Get next prompt number from user_prompts count
|
||||
const currentCount = store.getPromptNumberFromUserPrompts(claudeSessionId);
|
||||
const currentCount = store.getPromptNumberFromUserPrompts(contentSessionId);
|
||||
const promptNumber = currentCount + 1;
|
||||
|
||||
logger.info('HTTP', 'SessionRoutes: Calculated promptNumber', {
|
||||
@@ -598,7 +598,7 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
}
|
||||
|
||||
// Step 5: Save cleaned user prompt
|
||||
store.saveUserPrompt(claudeSessionId, promptNumber, cleanedPrompt);
|
||||
store.saveUserPrompt(contentSessionId, promptNumber, cleanedPrompt);
|
||||
|
||||
logger.info('SESSION', 'Session initialized via HTTP', {
|
||||
sessionId: sessionDbId,
|
||||
|
||||
@@ -12,20 +12,20 @@ export class PrivacyCheckValidator {
|
||||
* Check if user prompt is public (not entirely private)
|
||||
*
|
||||
* @param store - SessionStore instance
|
||||
* @param claudeSessionId - Claude session ID
|
||||
* @param contentSessionId - Claude session ID
|
||||
* @param promptNumber - Prompt number within session
|
||||
* @param operationType - Type of operation being validated ('observation' or 'summarize')
|
||||
* @returns User prompt text if public, null if private
|
||||
*/
|
||||
static checkUserPromptPrivacy(
|
||||
store: SessionStore,
|
||||
claudeSessionId: string,
|
||||
contentSessionId: string,
|
||||
promptNumber: number,
|
||||
operationType: 'observation' | 'summarize',
|
||||
sessionDbId: number,
|
||||
additionalContext?: Record<string, any>
|
||||
): string | null {
|
||||
const userPrompt = store.getUserPrompt(claudeSessionId, promptNumber);
|
||||
const userPrompt = store.getUserPrompt(contentSessionId, promptNumber);
|
||||
|
||||
if (!userPrompt || userPrompt.trim() === '') {
|
||||
logger.debug('HOOK', `Skipping ${operationType} - user prompt was entirely private`, {
|
||||
|
||||
Reference in New Issue
Block a user