Release v3.9.11

Published from npm package build
Source: https://github.com/thedotmack/claude-mem-source
This commit is contained in:
Alex Newman
2025-10-03 20:55:36 -04:00
parent 5b30764fa8
commit 5244a12422
13 changed files with 1136 additions and 1027 deletions
@@ -0,0 +1,266 @@
import { Database } from 'better-sqlite3';
import { getDatabase } from './Database.js';
import { normalizeTimestamp } from './types.js';
/**
* Represents a streaming session row in the database
*/
export interface StreamingSessionRow {
id: number;
claude_session_id: string;
sdk_session_id?: string;
project: string;
title?: string;
subtitle?: string;
user_prompt?: string;
started_at: string;
started_at_epoch: number;
updated_at?: string;
updated_at_epoch?: number;
completed_at?: string;
completed_at_epoch?: number;
status: 'active' | 'completed' | 'failed';
}
/**
* Input type for creating a new streaming session
*/
export interface StreamingSessionInput {
claude_session_id: string;
project: string;
user_prompt?: string;
started_at?: string | Date | number;
}
/**
* Input type for updating a streaming session
*/
export interface StreamingSessionUpdate {
sdk_session_id?: string;
title?: string;
subtitle?: string;
status?: 'active' | 'completed' | 'failed';
completed_at?: string | Date | number;
}
/**
* Data Access Object for streaming session records
* Handles real-time session tracking during SDK compression
*/
export class StreamingSessionStore {
private db: Database.Database;
constructor(db?: Database.Database) {
this.db = db || getDatabase();
}
/**
* Create a new streaming session record
* This should be called immediately when the hook receives a user prompt
*/
create(input: StreamingSessionInput): StreamingSessionRow {
const { isoString, epoch } = normalizeTimestamp(input.started_at);
const stmt = this.db.prepare(`
INSERT INTO streaming_sessions (
claude_session_id, project, user_prompt, started_at, started_at_epoch, status
) VALUES (?, ?, ?, ?, ?, 'active')
`);
const info = stmt.run(
input.claude_session_id,
input.project,
input.user_prompt || null,
isoString,
epoch
);
return this.getById(info.lastInsertRowid as number)!;
}
/**
* Update a streaming session by internal ID
* Uses atomic transaction to prevent race conditions
*/
update(id: number, updates: StreamingSessionUpdate): StreamingSessionRow {
const { isoString: updatedAt, epoch: updatedEpoch } = normalizeTimestamp(new Date());
const existing = this.getById(id);
if (!existing) {
throw new Error(`Streaming session with id ${id} not found`);
}
const parts: string[] = [];
const values: any[] = [];
if (updates.sdk_session_id !== undefined) {
parts.push('sdk_session_id = ?');
values.push(updates.sdk_session_id);
}
if (updates.title !== undefined) {
parts.push('title = ?');
values.push(updates.title);
}
if (updates.subtitle !== undefined) {
parts.push('subtitle = ?');
values.push(updates.subtitle);
}
if (updates.status !== undefined) {
parts.push('status = ?');
values.push(updates.status);
}
if (updates.completed_at !== undefined) {
const { isoString, epoch } = normalizeTimestamp(updates.completed_at);
parts.push('completed_at = ?', 'completed_at_epoch = ?');
values.push(isoString, epoch);
}
// Always update the updated_at timestamp
parts.push('updated_at = ?', 'updated_at_epoch = ?');
values.push(updatedAt, updatedEpoch);
values.push(id);
const stmt = this.db.prepare(`
UPDATE streaming_sessions
SET ${parts.join(', ')}
WHERE id = ?
`);
stmt.run(...values);
return this.getById(id)!;
}
/**
* Update a streaming session by Claude session ID
* Convenience method for hooks that only have the Claude session ID
*/
updateByClaudeSessionId(claudeSessionId: string, updates: StreamingSessionUpdate): StreamingSessionRow | null {
const session = this.getByClaudeSessionId(claudeSessionId);
if (!session) {
return null;
}
return this.update(session.id, updates);
}
/**
* Get streaming session by internal ID
*/
getById(id: number): StreamingSessionRow | null {
const stmt = this.db.prepare('SELECT * FROM streaming_sessions WHERE id = ?');
return stmt.get(id) as StreamingSessionRow || null;
}
/**
* Get streaming session by Claude session ID
*/
getByClaudeSessionId(claudeSessionId: string): StreamingSessionRow | null {
const stmt = this.db.prepare('SELECT * FROM streaming_sessions WHERE claude_session_id = ?');
return stmt.get(claudeSessionId) as StreamingSessionRow || null;
}
/**
* Get streaming session by SDK session ID
*/
getBySdkSessionId(sdkSessionId: string): StreamingSessionRow | null {
const stmt = this.db.prepare('SELECT * FROM streaming_sessions WHERE sdk_session_id = ?');
return stmt.get(sdkSessionId) as StreamingSessionRow || null;
}
/**
* Check if a streaming session exists by Claude session ID
*/
has(claudeSessionId: string): boolean {
const stmt = this.db.prepare('SELECT 1 FROM streaming_sessions WHERE claude_session_id = ? LIMIT 1');
return Boolean(stmt.get(claudeSessionId));
}
/**
* Get active streaming sessions for a project
*/
getActiveForProject(project: string): StreamingSessionRow[] {
const stmt = this.db.prepare(`
SELECT * FROM streaming_sessions
WHERE project = ? AND status = 'active'
ORDER BY started_at_epoch DESC
`);
return stmt.all(project) as StreamingSessionRow[];
}
/**
* Get all active streaming sessions
*/
getAllActive(): StreamingSessionRow[] {
const stmt = this.db.prepare(`
SELECT * FROM streaming_sessions
WHERE status = 'active'
ORDER BY started_at_epoch DESC
`);
return stmt.all() as StreamingSessionRow[];
}
/**
* Get recent streaming sessions (completed or failed)
*/
getRecent(limit = 10): StreamingSessionRow[] {
const stmt = this.db.prepare(`
SELECT * FROM streaming_sessions
ORDER BY started_at_epoch DESC
LIMIT ?
`);
return stmt.all(limit) as StreamingSessionRow[];
}
/**
* Mark a session as completed
*/
markCompleted(id: number): StreamingSessionRow {
return this.update(id, {
status: 'completed',
completed_at: new Date()
});
}
/**
* Mark a session as failed
*/
markFailed(id: number): StreamingSessionRow {
return this.update(id, {
status: 'failed',
completed_at: new Date()
});
}
/**
* Delete a streaming session by ID
*/
deleteById(id: number): boolean {
const stmt = this.db.prepare('DELETE FROM streaming_sessions WHERE id = ?');
const info = stmt.run(id);
return info.changes > 0;
}
/**
* Delete a streaming session by Claude session ID
*/
deleteByClaudeSessionId(claudeSessionId: string): boolean {
const stmt = this.db.prepare('DELETE FROM streaming_sessions WHERE claude_session_id = ?');
const info = stmt.run(claudeSessionId);
return info.changes > 0;
}
/**
* Clean up old completed/failed sessions (older than N days)
*/
cleanupOldSessions(daysOld = 30): number {
const cutoffEpoch = Date.now() - (daysOld * 24 * 60 * 60 * 1000);
const stmt = this.db.prepare(`
DELETE FROM streaming_sessions
WHERE status IN ('completed', 'failed')
AND completed_at_epoch < ?
`);
const info = stmt.run(cutoffEpoch);
return info.changes;
}
}
+5 -2
View File
@@ -7,6 +7,7 @@ export { MemoryStore } from './MemoryStore.js';
export { OverviewStore } from './OverviewStore.js';
export { DiagnosticsStore } from './DiagnosticsStore.js';
export { TranscriptEventStore } from './TranscriptEventStore.js';
export { StreamingSessionStore } from './StreamingSessionStore.js';
// Export types
export * from './types.js';
@@ -26,18 +27,20 @@ export async function createStores() {
}
const db = await manager.initialize();
const { SessionStore } = await import('./SessionStore.js');
const { MemoryStore } = await import('./MemoryStore.js');
const { OverviewStore } = await import('./OverviewStore.js');
const { DiagnosticsStore } = await import('./DiagnosticsStore.js');
const { TranscriptEventStore } = await import('./TranscriptEventStore.js');
const { StreamingSessionStore } = await import('./StreamingSessionStore.js');
return {
sessions: new SessionStore(db),
memories: new MemoryStore(db),
overviews: new OverviewStore(db),
diagnostics: new DiagnosticsStore(db),
transcriptEvents: new TranscriptEventStore(db)
transcriptEvents: new TranscriptEventStore(db),
streamingSessions: new StreamingSessionStore(db)
};
}
+44 -1
View File
@@ -160,10 +160,53 @@ export const migration002: Migration = {
}
};
/**
* Migration 003 - Add streaming_sessions table for real-time session tracking
*/
export const migration003: Migration = {
version: 3,
up: (db: Database.Database) => {
// Streaming sessions table - tracks active SDK compression sessions
db.exec(`
CREATE TABLE IF NOT EXISTS streaming_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
claude_session_id TEXT UNIQUE NOT NULL,
sdk_session_id TEXT,
project TEXT NOT NULL,
title TEXT,
subtitle TEXT,
user_prompt TEXT,
started_at TEXT NOT NULL,
started_at_epoch INTEGER NOT NULL,
updated_at TEXT,
updated_at_epoch INTEGER,
completed_at TEXT,
completed_at_epoch INTEGER,
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_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);
`);
console.log('✅ Created streaming_sessions table for real-time session tracking');
},
down: (db: Database.Database) => {
db.exec(`
DROP TABLE IF EXISTS streaming_sessions;
`);
}
};
/**
* All migrations in order
*/
export const migrations: Migration[] = [
migration001,
migration002
migration002,
migration003
];