refactor: remove sessions names db

This commit is contained in:
Haileyesus
2026-03-28 11:06:37 +03:00
parent 6cfe617711
commit ce0dfad638
16 changed files with 196 additions and 201 deletions

View File

@@ -5,7 +5,7 @@ import path from 'path';
import os from 'os';
import TOML from '@iarna/toml';
import { getCodexSessions, deleteCodexSession } from '../../../projects.js';
import { applyCustomSessionNames, sessionNamesDb } from '@/shared/database/repositories/session-names.js';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
const router = express.Router();
@@ -60,7 +60,7 @@ router.get('/sessions', async (req, res) => {
}
const sessions = await getCodexSessions(projectPath);
applyCustomSessionNames(sessions, 'codex');
sessionsDb.applyCustomSessionNames(sessions, 'codex');
res.json({ success: true, sessions });
} catch (error) {
console.error('Error fetching Codex sessions:', error);
@@ -72,7 +72,7 @@ router.delete('/sessions/:sessionId', async (req, res) => {
try {
const { sessionId } = req.params;
await deleteCodexSession(sessionId);
sessionNamesDb.deleteSessionName(sessionId, 'codex');
sessionsDb.deleteSession(sessionId);
res.json({ success: true });
} catch (error) {
console.error(`Error deleting Codex session ${req.params.sessionId}:`, error);

View File

@@ -7,7 +7,7 @@ import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
import crypto from 'crypto';
import { CURSOR_MODELS } from '../../../../shared/modelConstants.js';
import { applyCustomSessionNames } from '@/shared/database/repositories/session-names.js';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
const router = express.Router();
@@ -561,7 +561,7 @@ router.get('/sessions', async (req, res) => {
return new Date(b.createdAt) - new Date(a.createdAt);
});
applyCustomSessionNames(sessions, 'cursor');
sessionsDb.applyCustomSessionNames(sessions, 'cursor');
res.json({
success: true,

View File

@@ -1,6 +1,6 @@
import express from 'express';
import sessionManager from '../../../sessionManager.js';
import { sessionNamesDb } from '@/shared/database/repositories/session-names.js';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
const router = express.Router();
@@ -13,7 +13,7 @@ router.delete('/sessions/:sessionId', async (req, res) => {
}
await sessionManager.deleteSession(sessionId);
sessionNamesDb.deleteSessionName(sessionId, 'gemini');
sessionsDb.deleteSession(sessionId);
res.json({ success: true });
} catch (error) {
console.error(`Error deleting Gemini session ${req.params.sessionId}:`, error);

View File

@@ -10,7 +10,7 @@ import {
deleteProject,
searchConversations
} from '../../../projects.js';
import { applyCustomSessionNames, sessionNamesDb } from '@/shared/database/repositories/session-names.js';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
import { workspaceOriginalPathsDb } from '@/shared/database/repositories/workspace-original-paths.db.js';
import { authenticateToken } from '../auth/auth.middleware.js';
import { getWorkspaceNameFromPath, WORKSPACES_ROOT, validateWorkspacePath } from './projects.utils.js';
@@ -46,7 +46,7 @@ router.get('/api/projects/:projectName/sessions', authenticateToken, async (req,
try {
const { limit = 5, offset = 0 } = req.query;
const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
applyCustomSessionNames(result.sessions, 'claude');
sessionsDb.applyCustomSessionNames(result.sessions, 'claude');
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
@@ -70,7 +70,7 @@ router.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToke
const { projectName, sessionId } = req.params;
console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`);
await deleteSession(projectName, sessionId);
sessionNamesDb.deleteSessionName(sessionId, 'claude');
sessionsDb.deleteSession(sessionId);
console.log(`[API] Session ${sessionId} deleted successfully`);
res.json({ success: true });
} catch (error) {

View File

@@ -1,5 +1,6 @@
import os from 'os';
import path from 'path';
import fsp from 'node:fs/promises';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
import { buildLookupMap, extractFirstValidJsonlData, findFilesRecursivelyCreatedAfterLastScan } from '@/modules/providers/shared/session-parser.utils.js';
import { SessionData } from '@/shared/types/session.js';
@@ -28,7 +29,23 @@ export async function processClaudeSessions() {
const result = await processClaudeSessionFile(file, nameMap);
if (result) {
sessionsDb.createSession(result.sessionId, 'claude', result.workspacePath, result.sessionName);
let createdAt: string | undefined;
let updatedAt: string | undefined;
try {
const stat = await fsp.stat(file);
createdAt = stat.birthtime.toISOString();
updatedAt = stat.mtime.toISOString();
} catch {
// Ignore stat failures and let DB defaults handle created_at/updated_at.
}
sessionsDb.createSession(
result.sessionId,
'claude',
result.workspacePath,
result.sessionName,
createdAt,
updatedAt,
);
}
}
}

View File

@@ -1,5 +1,6 @@
import os from 'os';
import path from 'path';
import fsp from 'node:fs/promises';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
import { buildLookupMap, extractFirstValidJsonlData, findFilesRecursivelyCreatedAfterLastScan } from '@/modules/providers/shared/session-parser.utils.js';
import { SessionData } from '@/shared/types/session.js';
@@ -29,7 +30,23 @@ export async function processCodexSessions() {
const result = await processCodexSessionFile(file, nameMap);
if (result) {
sessionsDb.createSession(result.sessionId, 'codex', result.workspacePath, result.sessionName);
let createdAt: string | undefined;
let updatedAt: string | undefined;
try {
const stat = await fsp.stat(file);
createdAt = stat.birthtime.toISOString();
updatedAt = stat.mtime.toISOString();
} catch {
// Ignore stat failures and let DB defaults handle created_at/updated_at.
}
sessionsDb.createSession(
result.sessionId,
'codex',
result.workspacePath,
result.sessionName,
createdAt,
updatedAt,
);
}
}
}

View File

@@ -80,7 +80,23 @@ export async function processCursorSessions() {
const result = await processCursorSessionFile(file);
if (result) {
sessionsDb.createSession(result.sessionId, 'cursor', result.workspacePath, result.sessionName);
let createdAt: string | undefined;
let updatedAt: string | undefined;
try {
const stat = await fsp.stat(file);
createdAt = stat.birthtime.toISOString();
updatedAt = stat.mtime.toISOString();
} catch {
// Ignore stat failures and let DB defaults handle created_at/updated_at.
}
sessionsDb.createSession(
result.sessionId,
'cursor',
result.workspacePath,
result.sessionName,
createdAt,
updatedAt,
);
}
}
}

View File

@@ -31,7 +31,23 @@ export async function processGeminiSessions() {
for (const file of files) {
const result = await processGeminiSessionFile(file);
if (result) {
sessionsDb.createSession(result.sessionId, 'gemini', result.workspacePath, result.sessionName);
let createdAt: string | undefined;
let updatedAt: string | undefined;
try {
const stat = await fsp.stat(file);
createdAt = stat.birthtime.toISOString();
updatedAt = stat.mtime.toISOString();
} catch {
// Ignore stat failures and let DB defaults handle created_at/updated_at.
}
sessionsDb.createSession(
result.sessionId,
'gemini',
result.workspacePath,
result.sessionName,
createdAt,
updatedAt,
);
}
}
}

View File

@@ -2,7 +2,7 @@ import express from 'express';
import path from 'path';
import os from 'os';
import { promises as fsPromises } from 'fs';
import { sessionNamesDb } from '@/shared/database/repositories/session-names.js';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
import { extractProjectDirectory } from '../../../projects.js';
import { authenticateToken } from '../auth/auth.middleware.js';
@@ -27,7 +27,7 @@ router.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res
if (!provider || !VALID_PROVIDERS.includes(provider)) {
return res.status(400).json({ error: `Provider must be one of: ${VALID_PROVIDERS.join(', ')}` });
}
sessionNamesDb.createSessionName(safeSessionId, provider, summary.trim());
sessionsDb.createSessionName(safeSessionId, provider, summary.trim());
res.json({ success: true });
} catch (error) {
console.error(`[API] Error renaming session ${req.params.sessionId}:`, error);

View File

@@ -100,7 +100,24 @@ const onUpdate = async (
}
if (sessionId && workspacePath) {
sessionsDb.createSession(sessionId, provider, workspacePath, sessionName);
let createdAt: string | undefined;
let updatedAt: string | undefined;
try {
const stat = await fsPromises.stat(filePath);
createdAt = stat.birthtime.toISOString();
updatedAt = stat.mtime.toISOString();
} catch {
// Ignore stat failures and let DB defaults handle created_at/updated_at.
}
sessionsDb.createSession(
sessionId,
provider,
workspacePath,
sessionName,
createdAt,
updatedAt,
);
}
break;
}

View File

@@ -2,7 +2,7 @@ import webPush from 'web-push';
import { notificationPreferencesDb } from '@/shared/database/repositories/notification-preferences.js';
import { pushSubscriptionsDb } from '@/shared/database/repositories/push-subscriptions.js';
import { sessionNamesDb } from '@/shared/database/repositories/session-names.js';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
type NotificationKind = 'action_required' | 'stop' | 'error' | 'info' | string;
@@ -126,7 +126,7 @@ function resolveSessionName(event: NotificationEvent): string | null {
return null;
}
return normalizeSessionName(sessionNamesDb.getSessionName(event.sessionId, event.provider));
return normalizeSessionName(sessionsDb.getSessionName(event.sessionId, event.provider));
}
function buildPushBody(event: NotificationEvent) {
@@ -314,4 +314,3 @@ export {
notifyRunStopped,
notifyRunFailed,
};

View File

@@ -3,7 +3,6 @@ import {
APP_CONFIG_TABLE_SCHEMA_SQL,
LAST_SCANNED_AT_SQL,
PUSH_SUBSCRIPTIONS_TABLE_SCHEMA_SQL,
SESSION_NAMES_TABLE_SCHEMA_SQL,
SESSIONS_TABLE_SCHEMA_SQL,
USER_NOTIFICATION_PREFERENCES_TABLE_SCHEMA_SQL,
VAPID_KEYS_TABLE_SCHEMA_SQL,
@@ -45,15 +44,17 @@ export const runMigrations = (db: Database) => {
db.exec(PUSH_SUBSCRIPTIONS_TABLE_SCHEMA_SQL);
db.exec("CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user_id ON push_subscriptions(user_id)");
// Create session_names table if it doesn't exist (for existing installations)
db.exec(SESSION_NAMES_TABLE_SCHEMA_SQL);
db.exec("CREATE INDEX IF NOT EXISTS idx_session_names_lookup ON session_names(session_id, provider)");
// Create sessions table if it doesn't exist (for existing installations)
db.exec(SESSIONS_TABLE_SCHEMA_SQL);
db.exec(
"CREATE INDEX IF NOT EXISTS idx_session_ids_lookup ON sessions(session_id)"
);
const sessionsTableInfo = db.prepare("PRAGMA table_info(sessions)").all() as { name: string }[];
const sessionColumnNames = sessionsTableInfo.map((col) => col.name);
addColumnToTableIfNotExists(db, "sessions", sessionColumnNames, "created_at", "DATETIME");
addColumnToTableIfNotExists(db, "sessions", sessionColumnNames, "updated_at", "DATETIME");
db.exec("UPDATE sessions SET created_at = COALESCE(created_at, CURRENT_TIMESTAMP)");
db.exec("UPDATE sessions SET updated_at = COALESCE(updated_at, CURRENT_TIMESTAMP)");
db.exec(WORK_SPACE_PATH_SQL);
const workspaceOriginalPathsTableInfo = db.prepare("PRAGMA table_info(workspace_original_paths)").all() as { name: string }[];

View File

@@ -1,106 +0,0 @@
/**
* Session names repository.
*
* Stores provider-scoped custom names for sessions and exposes helpers
* to overlay those names onto in-memory session lists.
*/
import { getConnection } from '@/shared/database/connection.js';
import type {
SessionNameLookupRow,
SessionWithSummary,
} from '@/shared/database/types.js';
export const sessionNamesDb = {
/** Upserts a custom session name for a provider-scoped session id. */
createSessionName(sessionId: string, provider: string, customName: string): void {
const db = getConnection();
db.prepare(
`INSERT INTO session_names (session_id, provider, custom_name)
VALUES (?, ?, ?)
ON CONFLICT(session_id, provider)
DO UPDATE SET custom_name = excluded.custom_name, updated_at = CURRENT_TIMESTAMP`
).run(sessionId, provider, customName);
},
/** Alias to keep write semantics explicit when callers perform edits. */
updateSessionName(sessionId: string, provider: string, customName: string): void {
sessionNamesDb.createSessionName(sessionId, provider, customName);
},
/** Returns a custom name for one session/provider pair or null if unset. */
getSessionName(sessionId: string, provider: string): string | null {
const db = getConnection();
const row = db
.prepare(
'SELECT custom_name FROM session_names WHERE session_id = ? AND provider = ?'
)
.get(sessionId, provider) as { custom_name: string } | undefined;
return row?.custom_name ?? null;
},
/**
* Batch lookup for multiple session ids.
* Returns a Map<sessionId, customName> for efficient overlay onto lists.
*/
getSessionNames(sessionIds: string[], provider: string): Map<string, string> {
if (sessionIds.length === 0) return new Map();
const db = getConnection();
const placeholders = sessionIds.map(() => '?').join(',');
const rows = db
.prepare(
`SELECT session_id, custom_name FROM session_names
WHERE session_id IN (${placeholders}) AND provider = ?`
)
.all(...sessionIds, provider) as SessionNameLookupRow[];
return new Map(rows.map((row) => [row.session_id, row.custom_name]));
},
/** Deletes a custom name. Returns true if a row was removed. */
deleteSessionName(sessionId: string, provider: string): boolean {
const db = getConnection();
return (
db
.prepare(
'DELETE FROM session_names WHERE session_id = ? AND provider = ?'
)
.run(sessionId, provider).changes > 0
);
},
// Legacy aliases used by existing routes/services
setName(sessionId: string, provider: string, customName: string): void {
sessionNamesDb.createSessionName(sessionId, provider, customName);
},
getName(sessionId: string, provider: string): string | null {
return sessionNamesDb.getSessionName(sessionId, provider);
},
getNames(sessionIds: string[], provider: string): Map<string, string> {
return sessionNamesDb.getSessionNames(sessionIds, provider);
},
deleteName(sessionId: string, provider: string): boolean {
return sessionNamesDb.deleteSessionName(sessionId, provider);
},
};
/**
* Overlay custom names onto a session list in place.
* If a custom name exists, `summary` is replaced.
*/
export function applyCustomSessionNames(
sessions: SessionWithSummary[] | undefined | null,
provider: string
): void {
if (!sessions?.length) return;
const ids = sessions.map((session) => session.id);
const customNames = sessionNamesDb.getSessionNames(ids, provider);
for (const session of sessions) {
const customName = customNames.get(session.id);
if (customName) {
session.summary = customName;
}
}
}

View File

@@ -1,22 +1,92 @@
import { workspaceOriginalPathsDb } from '@/shared/database/repositories/workspace-original-paths.db.js';
import { getConnection } from '@/shared/database/connection.js';
import type { SessionWithSummary } from '@/shared/database/types.js';
// ---------------------------------------------------------------------------
// Queries
// ---------------------------------------------------------------------------
type SessionNameLookupRow = {
session_id: string;
custom_name: string;
};
function normalizeTimestamp(value?: string): string | null {
if (!value) return null;
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) {
return null;
}
return parsed.toISOString();
}
export const sessionsDb = {
createSession(session_id: string, provider: string, workspacePath: string, customName?: string): void {
createSession(
session_id: string,
provider: string,
workspacePath: string,
customName?: string,
createdAt?: string,
updatedAt?: string,
): void {
const db = getConnection();
const createdAtValue = normalizeTimestamp(createdAt);
const updatedAtValue = normalizeTimestamp(updatedAt);
// First, ensure the workspace path is recorded in the workspace_original_paths table
// since it's a foreign key in the sessions table.
workspaceOriginalPathsDb.createWorkspacePath(workspacePath);
db.prepare(
'INSERT OR IGNORE INTO sessions (session_id, provider, custom_name, workspace_path) VALUES (?, ?, ?, ?)'
).run(session_id, provider, customName, workspacePath);
`INSERT INTO sessions (session_id, provider, custom_name, workspace_path, created_at, updated_at)
VALUES (?, ?, ?, ?, COALESCE(?, CURRENT_TIMESTAMP), COALESCE(?, CURRENT_TIMESTAMP))
ON CONFLICT(session_id) DO UPDATE SET updated_at = excluded.updated_at
WHERE sessions.provider = excluded.provider`
).run(session_id, provider, customName, workspacePath, createdAtValue, updatedAtValue);
},
/** Updates a custom session name for an existing session row. */
createSessionName(sessionId: string, provider: string, customName: string): void {
const db = getConnection();
db.prepare(
`UPDATE sessions
SET custom_name = ?, updated_at = CURRENT_TIMESTAMP
WHERE session_id = ? AND provider = ?`
).run(customName, sessionId, provider);
},
getSessionName(sessionId: string, provider: string): string | null {
const db = getConnection();
const row = db
.prepare(
`SELECT custom_name
FROM sessions
WHERE session_id = ? AND provider = ?`
)
.get(sessionId, provider) as { custom_name: string | null } | undefined;
return row?.custom_name ?? null;
},
getSessionNames(sessionIds: string[], provider: string): Map<string, string> {
if (sessionIds.length === 0) return new Map();
const db = getConnection();
const placeholders = sessionIds.map(() => '?').join(',');
const rows = db
.prepare(
`SELECT session_id, custom_name
FROM sessions
WHERE session_id IN (${placeholders})
AND provider = ?
AND custom_name IS NOT NULL`
)
.all(...sessionIds, provider) as SessionNameLookupRow[];
return new Map(rows.map((row) => [row.session_id, row.custom_name]));
},
deleteSession(session_id: string): void {
@@ -24,59 +94,18 @@ export const sessionsDb = {
db.prepare('DELETE FROM sessions WHERE session_id = ?').run(session_id);
},
applyCustomSessionNames(sessions: SessionWithSummary[] | undefined | null, provider: string): void {
if (!sessions?.length) return;
// /** Inserts or updates a custom session name (upsert on session_id + provider). */
// setName(sessionId: string, provider: string, customName: string): void {
// const db = getConnection();
// db.prepare(
// `INSERT INTO session_names (session_id, provider, custom_name)
// VALUES (?, ?, ?)
// ON CONFLICT(session_id, provider)
// DO UPDATE SET custom_name = excluded.custom_name,
// updated_at = CURRENT_TIMESTAMP`
// ).run(sessionId, provider, customName);
// },
const ids = sessions.map((session) => session.id);
const customNames = sessionsDb.getSessionNames(ids, provider);
/** Returns the custom name for a single session, or null if unset. */
// getName(sessionId: string, provider: string): string | null {
// const db = getConnection();
// const row = db
// .prepare(
// 'SELECT custom_name FROM session_names WHERE session_id = ? AND provider = ?'
// )
// .get(sessionId, provider) as { custom_name: string } | undefined;
// return row?.custom_name ?? null;
// },
/**
* Batch lookup for multiple session IDs.
* Returns a Map<sessionId, customName> for efficient overlay onto session lists.
*/
// getNames(sessionIds: string[], provider: string): Map<string, string> {
// if (sessionIds.length === 0) return new Map();
// const db = getConnection();
// const placeholders = sessionIds.map(() => '?').join(',');
// const rows = db
// .prepare(
// `SELECT session_id, custom_name FROM session_names
// WHERE session_id IN (${placeholders}) AND provider = ?`
// )
// .all(...sessionIds, provider) as SessionNameLookupRow[];
// return new Map(rows.map((r) => [r.session_id, r.custom_name]));
// },
/** Removes a custom session name. Returns true if a row was deleted. */
// deleteName(sessionId: string, provider: string): boolean {
// const db = getConnection();
// return (
// db
// .prepare(
// 'DELETE FROM session_names WHERE session_id = ? AND provider = ?'
// )
// .run(sessionId, provider).changes > 0
// );
// },
for (const session of sessions) {
const customName = customNames.get(session.id);
if (customName) {
session.summary = customName;
}
}
},
};

View File

@@ -69,24 +69,14 @@ CREATE TABLE IF NOT EXISTS push_subscriptions (
);
`;
export const SESSION_NAMES_TABLE_SCHEMA_SQL = `
CREATE TABLE IF NOT EXISTS session_names (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
provider TEXT NOT NULL DEFAULT 'claude',
custom_name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(session_id, provider)
);
`;
export const SESSIONS_TABLE_SCHEMA_SQL = `
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY NOT NULL,
provider TEXT NOT NULL,
custom_name TEXT,
workspace_path TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (workspace_path) REFERENCES workspace_original_paths(workspace_path)
ON DELETE CASCADE
ON UPDATE CASCADE
@@ -143,9 +133,6 @@ ${VAPID_KEYS_TABLE_SCHEMA_SQL}
${PUSH_SUBSCRIPTIONS_TABLE_SCHEMA_SQL}
CREATE INDEX IF NOT EXISTS idx_push_subscriptions_user_id ON push_subscriptions(user_id);
${SESSION_NAMES_TABLE_SCHEMA_SQL}
CREATE INDEX IF NOT EXISTS idx_session_names_lookup ON session_names(session_id, provider);
${SESSIONS_TABLE_SCHEMA_SQL}
CREATE INDEX IF NOT EXISTS idx_session_ids_lookup ON sessions(session_id);

View File

@@ -101,6 +101,8 @@ export type SessionsRow = {
provider: LLMProvider;
workspace_path: string;
custom_name: string | null;
created_at: string;
updated_at: string;
};
export type SessionNameRow = {