Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2aae3d9db5 | |||
| de20eb65b5 | |||
| fef332d213 | |||
| 3b86d5ccad | |||
| e95cfca9aa | |||
| d9e966d8f4 | |||
| 61a23a14a9 |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "8.5.0",
|
||||
"version": "8.5.2",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
+409
-283
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Claude Agent SDK V2 Examples
|
||||
*
|
||||
* The V2 API provides a session-based interface with separate send()/receive(),
|
||||
* ideal for multi-turn conversations. Run with: npx tsx v2-examples.ts
|
||||
*/
|
||||
|
||||
import {
|
||||
unstable_v2_createSession,
|
||||
unstable_v2_resumeSession,
|
||||
unstable_v2_prompt,
|
||||
} from '@anthropic-ai/claude-agent-sdk';
|
||||
|
||||
async function main() {
|
||||
const example = process.argv[2] || 'basic';
|
||||
|
||||
switch (example) {
|
||||
case 'basic':
|
||||
await basicSession();
|
||||
break;
|
||||
case 'multi-turn':
|
||||
await multiTurn();
|
||||
break;
|
||||
case 'one-shot':
|
||||
await oneShot();
|
||||
break;
|
||||
case 'resume':
|
||||
await sessionResume();
|
||||
break;
|
||||
default:
|
||||
console.log('Usage: npx tsx v2-examples.ts [basic|multi-turn|one-shot|resume]');
|
||||
}
|
||||
}
|
||||
|
||||
// Basic session with send/receive pattern
|
||||
async function basicSession() {
|
||||
console.log('=== Basic Session ===\n');
|
||||
|
||||
await using session = unstable_v2_createSession({ model: 'sonnet' });
|
||||
await session.send('Hello! Introduce yourself in one sentence.');
|
||||
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
const text = msg.message.content.find((c): c is { type: 'text'; text: string } => c.type === 'text');
|
||||
console.log(`Claude: ${text?.text}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-turn conversation - V2's key advantage
|
||||
async function multiTurn() {
|
||||
console.log('=== Multi-Turn Conversation ===\n');
|
||||
|
||||
await using session = unstable_v2_createSession({ model: 'sonnet' });
|
||||
|
||||
// Turn 1
|
||||
await session.send('What is 5 + 3? Just the number.');
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
const text = msg.message.content.find((c): c is { type: 'text'; text: string } => c.type === 'text');
|
||||
console.log(`Turn 1: ${text?.text}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Turn 2 - Claude remembers context
|
||||
await session.send('Multiply that by 2. Just the number.');
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
const text = msg.message.content.find((c): c is { type: 'text'; text: string } => c.type === 'text');
|
||||
console.log(`Turn 2: ${text?.text}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One-shot convenience function
|
||||
async function oneShot() {
|
||||
console.log('=== One-Shot Prompt ===\n');
|
||||
|
||||
const result = await unstable_v2_prompt('What is the capital of France? One word.', { model: 'sonnet' });
|
||||
|
||||
if (result.subtype === 'success') {
|
||||
console.log(`Answer: ${result.result}`);
|
||||
console.log(`Cost: $${result.total_cost_usd.toFixed(4)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Session resume - persist context across sessions
|
||||
async function sessionResume() {
|
||||
console.log('=== Session Resume ===\n');
|
||||
|
||||
let sessionId: string | undefined;
|
||||
|
||||
// First session - establish a memory
|
||||
{
|
||||
await using session = unstable_v2_createSession({ model: 'sonnet' });
|
||||
console.log('[Session 1] Telling Claude my favorite color...');
|
||||
await session.send('My favorite color is blue. Remember this!');
|
||||
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'system' && msg.subtype === 'init') {
|
||||
sessionId = msg.session_id;
|
||||
console.log(`[Session 1] ID: ${sessionId}`);
|
||||
}
|
||||
if (msg.type === 'assistant') {
|
||||
const text = msg.message.content.find((c): c is { type: 'text'; text: string } => c.type === 'text');
|
||||
console.log(`[Session 1] Claude: ${text?.text}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('--- Session closed. Time passes... ---\n');
|
||||
|
||||
// Resume and verify Claude remembers
|
||||
{
|
||||
await using session = unstable_v2_resumeSession(sessionId!, { model: 'sonnet' });
|
||||
console.log('[Session 2] Resuming and asking Claude...');
|
||||
await session.send('What is my favorite color?');
|
||||
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
const text = msg.message.content.find((c): c is { type: 'text'; text: string } => c.type === 'text');
|
||||
console.log(`[Session 2] Claude: ${text?.text}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "8.5.0",
|
||||
"version": "8.5.2",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "8.5.0",
|
||||
"version": "8.5.2",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem-plugin",
|
||||
"version": "8.5.0",
|
||||
"version": "8.5.1",
|
||||
"private": true,
|
||||
"description": "Runtime dependencies for claude-mem bundled hooks",
|
||||
"type": "module",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+11
-11
@@ -12,7 +12,7 @@ import { SettingsDefaultsManager } from '../src/shared/SettingsDefaultsManager';
|
||||
|
||||
interface ObservationRecord {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string;
|
||||
project: string;
|
||||
text: string | null;
|
||||
type: string;
|
||||
@@ -31,8 +31,8 @@ interface ObservationRecord {
|
||||
|
||||
interface SdkSessionRecord {
|
||||
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;
|
||||
@@ -44,7 +44,7 @@ interface SdkSessionRecord {
|
||||
|
||||
interface SessionSummaryRecord {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string;
|
||||
project: string;
|
||||
request: string | null;
|
||||
investigated: string | null;
|
||||
@@ -62,7 +62,7 @@ interface SessionSummaryRecord {
|
||||
|
||||
interface UserPromptRecord {
|
||||
id: number;
|
||||
claude_session_id: string;
|
||||
content_session_id: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
created_at: string;
|
||||
@@ -117,23 +117,23 @@ async function exportMemories(query: string, outputFile: string, project?: strin
|
||||
console.log(`✅ Found ${summaries.length} session summaries`);
|
||||
console.log(`✅ Found ${prompts.length} user prompts`);
|
||||
|
||||
// Get unique SDK session IDs from observations and summaries
|
||||
const sdkSessionIds = new Set<string>();
|
||||
// Get unique memory session IDs from observations and summaries
|
||||
const memorySessionIds = new Set<string>();
|
||||
observations.forEach((o) => {
|
||||
if (o.sdk_session_id) sdkSessionIds.add(o.sdk_session_id);
|
||||
if (o.memory_session_id) memorySessionIds.add(o.memory_session_id);
|
||||
});
|
||||
summaries.forEach((s) => {
|
||||
if (s.sdk_session_id) sdkSessionIds.add(s.sdk_session_id);
|
||||
if (s.memory_session_id) memorySessionIds.add(s.memory_session_id);
|
||||
});
|
||||
|
||||
// Get SDK sessions metadata via API
|
||||
console.log('📡 Fetching SDK sessions metadata...');
|
||||
let sessions: SdkSessionRecord[] = [];
|
||||
if (sdkSessionIds.size > 0) {
|
||||
if (memorySessionIds.size > 0) {
|
||||
const sessionsResponse = await fetch(`${baseUrl}/api/sdk-sessions/batch`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sdkSessionIds: Array.from(sdkSessionIds) })
|
||||
body: JSON.stringify({ sdkSessionIds: Array.from(memorySessionIds) })
|
||||
});
|
||||
if (sessionsResponse.ok) {
|
||||
sessions = await sessionsResponse.json();
|
||||
|
||||
@@ -18,7 +18,7 @@ interface CorruptedObservation {
|
||||
obs_created: number;
|
||||
session_started: number;
|
||||
session_completed: number | null;
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string;
|
||||
}
|
||||
|
||||
function formatTimestamp(epoch: number): string {
|
||||
@@ -54,9 +54,9 @@ function main() {
|
||||
o.created_at_epoch as obs_created,
|
||||
s.started_at_epoch as session_started,
|
||||
s.completed_at_epoch as session_completed,
|
||||
s.sdk_session_id
|
||||
s.memory_session_id
|
||||
FROM observations o
|
||||
JOIN sdk_sessions s ON o.sdk_session_id = s.sdk_session_id
|
||||
JOIN sdk_sessions s ON o.memory_session_id = s.memory_session_id
|
||||
WHERE o.created_at_epoch < s.started_at_epoch -- Observation older than session
|
||||
OR (s.completed_at_epoch IS NOT NULL
|
||||
AND o.created_at_epoch > (s.completed_at_epoch + 3600000)) -- More than 1hr after session
|
||||
|
||||
@@ -20,7 +20,7 @@ const BAD_WINDOW_END = 1766626260000; // Dec 24 20:31 PST
|
||||
|
||||
interface AffectedObservation {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string;
|
||||
created_at_epoch: number;
|
||||
title: string;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ interface ProcessedMessage {
|
||||
|
||||
interface SessionMapping {
|
||||
session_db_id: number;
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string;
|
||||
}
|
||||
|
||||
interface TimestampFix {
|
||||
@@ -75,7 +75,7 @@ function main() {
|
||||
// Step 1: Find affected observations
|
||||
console.log('Step 1: Finding observations created during bad window...');
|
||||
const affectedObs = db.query<AffectedObservation, []>(`
|
||||
SELECT id, sdk_session_id, created_at_epoch, title
|
||||
SELECT id, memory_session_id, created_at_epoch, title
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ${BAD_WINDOW_START}
|
||||
AND created_at_epoch <= ${BAD_WINDOW_END}
|
||||
@@ -111,7 +111,7 @@ function main() {
|
||||
obs_title: string;
|
||||
obs_created: number;
|
||||
session_started: number;
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string;
|
||||
}
|
||||
|
||||
const obsWithSessions = db.query<ObsWithSession, []>(`
|
||||
@@ -120,9 +120,9 @@ function main() {
|
||||
o.title as obs_title,
|
||||
o.created_at_epoch as obs_created,
|
||||
s.started_at_epoch as session_started,
|
||||
s.sdk_session_id
|
||||
s.memory_session_id
|
||||
FROM observations o
|
||||
JOIN sdk_sessions s ON o.sdk_session_id = s.sdk_session_id
|
||||
JOIN sdk_sessions s ON o.memory_session_id = s.memory_session_id
|
||||
WHERE o.created_at_epoch >= ${BAD_WINDOW_START}
|
||||
AND o.created_at_epoch <= ${BAD_WINDOW_END}
|
||||
AND s.started_at_epoch < ${BAD_WINDOW_START}
|
||||
|
||||
@@ -36,7 +36,7 @@ function main() {
|
||||
const dec24End = 1735113600000; // Dec 25 00:00 PST
|
||||
|
||||
const dec24Obs = db.query(`
|
||||
SELECT id, sdk_session_id, created_at_epoch, title
|
||||
SELECT id, memory_session_id, created_at_epoch, title
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ${dec24Start}
|
||||
AND created_at_epoch < ${dec24End}
|
||||
@@ -59,7 +59,7 @@ function main() {
|
||||
const dec21Start = 1734768000000; // Dec 21 00:00 PST
|
||||
|
||||
const oldObs = db.query(`
|
||||
SELECT id, sdk_session_id, created_at_epoch, title
|
||||
SELECT id, memory_session_id, created_at_epoch, title
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ${dec17Start}
|
||||
AND created_at_epoch < ${dec21Start}
|
||||
|
||||
@@ -59,7 +59,7 @@ function main() {
|
||||
pm.tool_name,
|
||||
pm.created_at_epoch as msg_created,
|
||||
pm.status,
|
||||
s.sdk_session_id,
|
||||
s.memory_session_id,
|
||||
s.started_at_epoch as session_started,
|
||||
s.project
|
||||
FROM pending_messages pm
|
||||
|
||||
@@ -22,7 +22,7 @@ const ORIGINAL_WINDOW_END = 1766613600000; // Dec 23 23:59 PST
|
||||
|
||||
interface Observation {
|
||||
id: number;
|
||||
sdk_session_id: string;
|
||||
memory_session_id: string;
|
||||
created_at_epoch: number;
|
||||
created_at: string;
|
||||
title: string;
|
||||
@@ -49,7 +49,7 @@ function main() {
|
||||
// Check 1: Observations still in bad window
|
||||
console.log('Check 1: Looking for observations still in bad window (Dec 24 19:45-20:31)...');
|
||||
const badWindowObs = db.query<Observation, []>(`
|
||||
SELECT id, sdk_session_id, created_at_epoch, created_at, title
|
||||
SELECT id, memory_session_id, created_at_epoch, created_at, title
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ${BAD_WINDOW_START}
|
||||
AND created_at_epoch <= ${BAD_WINDOW_END}
|
||||
@@ -63,7 +63,7 @@ function main() {
|
||||
for (const obs of badWindowObs) {
|
||||
console.log(` Observation #${obs.id}: ${obs.title || '(no title)'}`);
|
||||
console.log(` Timestamp: ${formatTimestamp(obs.created_at_epoch)}`);
|
||||
console.log(` Session: ${obs.sdk_session_id}\n`);
|
||||
console.log(` Session: ${obs.memory_session_id}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,19 +81,19 @@ function main() {
|
||||
|
||||
// Check 3: Session distribution
|
||||
console.log('Check 3: Session distribution of corrected observations...');
|
||||
const sessionDist = db.query<{ sdk_session_id: string; count: number }, []>(`
|
||||
SELECT sdk_session_id, COUNT(*) as count
|
||||
const sessionDist = db.query<{ memory_session_id: string; count: number }, []>(`
|
||||
SELECT memory_session_id, COUNT(*) as count
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ${ORIGINAL_WINDOW_START}
|
||||
AND created_at_epoch <= ${ORIGINAL_WINDOW_END}
|
||||
GROUP BY sdk_session_id
|
||||
GROUP BY memory_session_id
|
||||
ORDER BY count DESC
|
||||
`).all();
|
||||
|
||||
if (sessionDist.length > 0) {
|
||||
console.log(`Observations distributed across ${sessionDist.length} sessions:\n`);
|
||||
for (const dist of sessionDist.slice(0, 10)) {
|
||||
console.log(` ${dist.sdk_session_id}: ${dist.count} observations`);
|
||||
console.log(` ${dist.memory_session_id}: ${dist.count} observations`);
|
||||
}
|
||||
if (sessionDist.length > 10) {
|
||||
console.log(` ... and ${sessionDist.length - 10} more sessions`);
|
||||
|
||||
@@ -582,126 +582,85 @@ 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)
|
||||
*
|
||||
* IDEMPOTENT: Checks each table individually before renaming.
|
||||
* This handles databases in any intermediate state (partial migration, fresh install, etc.)
|
||||
*/
|
||||
private renameSessionIdColumns(): void {
|
||||
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(17) as SchemaVersion | undefined;
|
||||
if (applied) return;
|
||||
|
||||
// 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');
|
||||
logger.info('DB', 'Checking session ID columns for semantic clarity rename');
|
||||
|
||||
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;
|
||||
}
|
||||
let renamesPerformed = 0;
|
||||
|
||||
logger.info('DB', 'Renaming session ID columns for semantic clarity');
|
||||
// Helper to safely rename a column if it exists
|
||||
const safeRenameColumn = (table: string, oldCol: string, newCol: string): boolean => {
|
||||
try {
|
||||
const tableInfo = this.db.query(`PRAGMA table_info(${table})`).all() as TableColumnInfo[];
|
||||
const hasOldCol = tableInfo.some(col => col.name === oldCol);
|
||||
const hasNewCol = tableInfo.some(col => col.name === newCol);
|
||||
|
||||
// Begin transaction for atomic rename
|
||||
this.db.run('BEGIN TRANSACTION');
|
||||
if (hasNewCol) {
|
||||
// Already renamed, nothing to do
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// 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');
|
||||
if (hasOldCol) {
|
||||
// SQLite 3.25+ supports ALTER TABLE RENAME COLUMN
|
||||
this.db.run(`ALTER TABLE ${table} RENAME COLUMN ${oldCol} TO ${newCol}`);
|
||||
logger.info('DB', `Renamed ${table}.${oldCol} to ${newCol}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Rename in pending_messages table
|
||||
this.db.run('ALTER TABLE pending_messages RENAME COLUMN claude_session_id TO content_session_id');
|
||||
// Neither column exists - table might not exist or has different schema
|
||||
logger.warn('DB', `Column ${oldCol} not found in ${table}, skipping rename`);
|
||||
return false;
|
||||
} catch (error: any) {
|
||||
// Table might not exist yet, which is fine
|
||||
logger.warn('DB', `Could not rename ${table}.${oldCol}: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Rename in observations table
|
||||
this.db.run('ALTER TABLE observations RENAME COLUMN sdk_session_id TO memory_session_id');
|
||||
// Rename in sdk_sessions table
|
||||
if (safeRenameColumn('sdk_sessions', 'claude_session_id', 'content_session_id')) renamesPerformed++;
|
||||
if (safeRenameColumn('sdk_sessions', 'sdk_session_id', 'memory_session_id')) renamesPerformed++;
|
||||
|
||||
// Rename in session_summaries table
|
||||
this.db.run('ALTER TABLE session_summaries RENAME COLUMN sdk_session_id TO memory_session_id');
|
||||
// Rename in pending_messages table
|
||||
if (safeRenameColumn('pending_messages', 'claude_session_id', 'content_session_id')) renamesPerformed++;
|
||||
|
||||
// Rename in user_prompts table
|
||||
this.db.run('ALTER TABLE user_prompts RENAME COLUMN claude_session_id TO content_session_id');
|
||||
// Rename in observations table
|
||||
if (safeRenameColumn('observations', 'sdk_session_id', 'memory_session_id')) renamesPerformed++;
|
||||
|
||||
// Commit transaction
|
||||
this.db.run('COMMIT');
|
||||
// Rename in session_summaries table
|
||||
if (safeRenameColumn('session_summaries', 'sdk_session_id', 'memory_session_id')) renamesPerformed++;
|
||||
|
||||
// Record migration
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(17, new Date().toISOString());
|
||||
// Rename in user_prompts table
|
||||
if (safeRenameColumn('user_prompts', 'claude_session_id', 'content_session_id')) renamesPerformed++;
|
||||
|
||||
logger.info('DB', 'Successfully renamed session ID columns');
|
||||
} catch (error: any) {
|
||||
// Rollback on error
|
||||
this.db.run('ROLLBACK');
|
||||
logger.error('DB', 'Session ID column rename migration error', undefined, error);
|
||||
throw error;
|
||||
// Record migration
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(17, new Date().toISOString());
|
||||
|
||||
if (renamesPerformed > 0) {
|
||||
logger.info('DB', `Successfully renamed ${renamesPerformed} session ID columns`);
|
||||
} else {
|
||||
logger.info('DB', 'No session ID column renames needed (already up to date)');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* DEPRECATED: Migration 17 is now fully idempotent and handles all cases.
|
||||
* This migration is kept for backwards compatibility but does nothing.
|
||||
*/
|
||||
private repairSessionIdColumnRename(): void {
|
||||
try {
|
||||
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(19) as SchemaVersion | undefined;
|
||||
if (applied) return;
|
||||
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;
|
||||
}
|
||||
// Migration 17 now handles all column rename cases idempotently.
|
||||
// Just record this migration as applied.
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(19, new Date().toISOString());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -168,8 +168,9 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
})
|
||||
.finally(() => {
|
||||
const sessionDbId = session.sessionDbId;
|
||||
|
||||
if (session.abortController.signal.aborted) {
|
||||
const wasAborted = session.abortController.signal.aborted;
|
||||
|
||||
if (wasAborted) {
|
||||
logger.info('SESSION', `Generator aborted`, { sessionId: sessionDbId });
|
||||
} else {
|
||||
logger.warn('SESSION', `Generator exited unexpectedly`, { sessionId: sessionDbId });
|
||||
@@ -180,16 +181,20 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
this.workerService.broadcastProcessingStatus();
|
||||
|
||||
// Crash recovery: If not aborted and still has work, restart
|
||||
if (!session.abortController.signal.aborted) {
|
||||
if (!wasAborted) {
|
||||
try {
|
||||
const pendingStore = this.sessionManager.getPendingMessageStore();
|
||||
const pendingCount = pendingStore.getPendingCount(sessionDbId);
|
||||
|
||||
|
||||
if (pendingCount > 0) {
|
||||
logger.info('SESSION', `Restarting generator after crash/exit with pending work`, {
|
||||
sessionId: sessionDbId,
|
||||
pendingCount
|
||||
});
|
||||
|
||||
// Create new AbortController for the restarted generator
|
||||
session.abortController = new AbortController();
|
||||
|
||||
// Small delay before restart
|
||||
setTimeout(() => {
|
||||
const stillExists = this.sessionManager.getSession(sessionDbId);
|
||||
@@ -197,12 +202,19 @@ export class SessionRoutes extends BaseRouteHandler {
|
||||
this.startGeneratorWithProvider(stillExists, this.getSelectedProvider(), 'crash-recovery');
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
// No pending work - abort to kill the child process
|
||||
session.abortController.abort();
|
||||
logger.debug('SESSION', 'Aborted controller after natural completion', {
|
||||
sessionId: sessionDbId
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors during recovery check
|
||||
// Ignore errors during recovery check, but still abort to prevent leaks
|
||||
session.abortController.abort();
|
||||
}
|
||||
}
|
||||
// NOTE: We do NOT delete the session here anymore.
|
||||
// NOTE: We do NOT delete the session here anymore.
|
||||
// The generator waits for events, so if it exited, it's either aborted or crashed.
|
||||
// Idle sessions stay in memory (ActiveSession is small) to listen for future events.
|
||||
});
|
||||
|
||||
@@ -37,9 +37,9 @@ describe('SessionStore', () => {
|
||||
const claudeId = 'claude-sess-obs';
|
||||
const sdkId = store.createSDKSession(claudeId, 'test-project', 'initial prompt');
|
||||
|
||||
// Get the sdk_session_id string (createSDKSession returns number ID, need string for FK)
|
||||
// Wait, createSDKSession inserts using sdk_session_id = claude_session_id in the current implementation
|
||||
// "VALUES (?, ?, ?, ?, ?, ?, 'active')" -> claudeSessionId, claudeSessionId, ...
|
||||
// Get the memory_session_id string (createSDKSession returns number ID, need string for FK)
|
||||
// createSDKSession inserts using memory_session_id = content_session_id in the current implementation
|
||||
// "VALUES (?, ?, ?, ?, ?, ?, 'active')" -> contentSessionId, contentSessionId, ...
|
||||
|
||||
const obs = {
|
||||
type: 'discovery',
|
||||
|
||||
@@ -7,11 +7,12 @@ describe('Refactor Validation: SQL Updates', () => {
|
||||
beforeEach(() => {
|
||||
db = new Database(':memory:');
|
||||
// Minimal schema for sdk_sessions based on SessionStore.ts migration004
|
||||
// Uses new column names: content_session_id and memory_session_id
|
||||
db.run(`
|
||||
CREATE TABLE 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,
|
||||
@@ -27,28 +28,26 @@ describe('Refactor Validation: SQL Updates', () => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
it('should update sdk_session_id using direct SQL (replacing updateSDKSessionId)', () => {
|
||||
// Setup initial state: A session without an sdk_session_id
|
||||
const claudeId = 'claude-session-123';
|
||||
const syntheticId = 'sdk-session-456';
|
||||
|
||||
it('should update memory_session_id using direct SQL (replacing updateSDKSessionId)', () => {
|
||||
// Setup initial state: A session without a memory_session_id
|
||||
const contentId = 'content-session-123';
|
||||
const memoryId = 'memory-session-456';
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO sdk_sessions (claude_session_id, project, started_at, started_at_epoch)
|
||||
INSERT INTO sdk_sessions (content_session_id, project, started_at, started_at_epoch)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).run(claudeId, 'test-project', '2025-01-01T00:00:00Z', 1735689600000);
|
||||
`).run(contentId, 'test-project', '2025-01-01T00:00:00Z', 1735689600000);
|
||||
|
||||
// Verify initial state
|
||||
const before = db.prepare('SELECT sdk_session_id FROM sdk_sessions WHERE claude_session_id = ?').get(claudeId) as any;
|
||||
expect(before.sdk_session_id).toBeNull();
|
||||
const before = db.prepare('SELECT memory_session_id FROM sdk_sessions WHERE content_session_id = ?').get(contentId) as any;
|
||||
expect(before.memory_session_id).toBeNull();
|
||||
|
||||
// EXECUTE: The exact SQL statement from the refactor in import-xml-observations.ts
|
||||
// Original code: db['db'].prepare('UPDATE sdk_sessions SET sdk_session_id = ? WHERE claude_session_id = ?').run(syntheticSdkSessionId, sessionMeta.sessionId);
|
||||
|
||||
const stmt = db.prepare('UPDATE sdk_sessions SET sdk_session_id = ? WHERE claude_session_id = ?');
|
||||
stmt.run(syntheticId, claudeId);
|
||||
// EXECUTE: The exact SQL statement from the refactor
|
||||
const stmt = db.prepare('UPDATE sdk_sessions SET memory_session_id = ? WHERE content_session_id = ?');
|
||||
stmt.run(memoryId, contentId);
|
||||
|
||||
// VERIFY: The update happened
|
||||
const after = db.prepare('SELECT sdk_session_id FROM sdk_sessions WHERE claude_session_id = ?').get(claudeId) as any;
|
||||
expect(after.sdk_session_id).toBe(syntheticId);
|
||||
const after = db.prepare('SELECT memory_session_id FROM sdk_sessions WHERE content_session_id = ?').get(contentId) as any;
|
||||
expect(after.memory_session_id).toBe(memoryId);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user