feat: isolate Claude and Codex session sources
Persist platform_source across session creation, transcript ingestion, API query paths, and viewer state so Claude and Codex data can coexist without bleeding into each other. - add platform-source normalization helpers and persist platform_source in sdk_sessions via migration 24 with backfill and indexing - thread platformSource through CLI hooks, transcript processing, context generation, pagination, search routes, SSE payloads, and session management - expose source-aware project catalogs, viewer tabs, context preview selectors, and source badges for observations, prompts, and summaries - start the transcript watcher from the worker for transcript-based clients and preserve platform source during Codex ingestion - auto-start the worker from the MCP server for MCP-only clients and tighten stdio-driven cleanup during shutdown - keep createSDKSession backward compatible with existing custom-title callers while allowing explicit platform source forwarding
This commit is contained in:
@@ -3,6 +3,7 @@ import { TableNameRow } from '../../types/database.js';
|
||||
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
import { isDirectChild } from '../../shared/path-utils.js';
|
||||
import { AppError } from '../server/ErrorHandler.js';
|
||||
import {
|
||||
ObservationSearchResult,
|
||||
SessionSummarySearchResult,
|
||||
@@ -22,6 +23,8 @@ import {
|
||||
export class SessionSearch {
|
||||
private db: Database;
|
||||
|
||||
private static readonly MISSING_SEARCH_INPUT_MESSAGE = 'Either query or filters required for search';
|
||||
|
||||
constructor(dbPath?: string) {
|
||||
if (!dbPath) {
|
||||
ensureDir(DATA_DIR);
|
||||
@@ -280,7 +283,7 @@ export class SessionSearch {
|
||||
if (!query) {
|
||||
const filterClause = this.buildFilterClause(filters, params, 'o');
|
||||
if (!filterClause) {
|
||||
throw new Error('Either query or filters required for search');
|
||||
throw new AppError(SessionSearch.MISSING_SEARCH_INPUT_MESSAGE, 400, 'INVALID_SEARCH_REQUEST');
|
||||
}
|
||||
|
||||
const orderClause = this.buildOrderClause(orderBy, false);
|
||||
@@ -317,7 +320,7 @@ export class SessionSearch {
|
||||
delete filterOptions.type;
|
||||
const filterClause = this.buildFilterClause(filterOptions, params, 's');
|
||||
if (!filterClause) {
|
||||
throw new Error('Either query or filters required for search');
|
||||
throw new AppError(SessionSearch.MISSING_SEARCH_INPUT_MESSAGE, 400, 'INVALID_SEARCH_REQUEST');
|
||||
}
|
||||
|
||||
const orderClause = orderBy === 'date_asc'
|
||||
@@ -551,7 +554,7 @@ export class SessionSearch {
|
||||
// FILTER-ONLY PATH: When no query text, query user_prompts table directly
|
||||
if (!query) {
|
||||
if (baseConditions.length === 0) {
|
||||
throw new Error('Either query or filters required for search');
|
||||
throw new AppError(SessionSearch.MISSING_SEARCH_INPUT_MESSAGE, 400, 'INVALID_SEARCH_REQUEST');
|
||||
}
|
||||
|
||||
const whereClause = `WHERE ${baseConditions.join(' AND ')}`;
|
||||
|
||||
@@ -14,6 +14,17 @@ import {
|
||||
} from '../../types/database.js';
|
||||
import type { PendingMessageStore } from './PendingMessageStore.js';
|
||||
import { computeObservationContentHash, findDuplicateObservation } from './observations/store.js';
|
||||
import { DEFAULT_PLATFORM_SOURCE, normalizePlatformSource, sortPlatformSources } from '../../shared/platform-source.js';
|
||||
|
||||
function resolveCreateSessionArgs(
|
||||
customTitle?: string,
|
||||
platformSource?: string
|
||||
): { customTitle?: string; platformSource: string } {
|
||||
return {
|
||||
customTitle,
|
||||
platformSource: platformSource ?? DEFAULT_PLATFORM_SOURCE
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Session data store for SDK sessions, observations, and summaries
|
||||
@@ -51,6 +62,7 @@ export class SessionStore {
|
||||
this.addOnUpdateCascadeToForeignKeys();
|
||||
this.addObservationContentHashColumn();
|
||||
this.addSessionCustomTitleColumn();
|
||||
this.addSessionPlatformSourceColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,6 +90,7 @@ export class SessionStore {
|
||||
content_session_id TEXT UNIQUE NOT NULL,
|
||||
memory_session_id TEXT UNIQUE,
|
||||
project TEXT NOT NULL,
|
||||
platform_source TEXT NOT NULL DEFAULT 'claude',
|
||||
user_prompt TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
started_at_epoch INTEGER NOT NULL,
|
||||
@@ -875,6 +888,31 @@ export class SessionStore {
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(23, new Date().toISOString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add platform_source column to sdk_sessions for Claude/Codex isolation (migration 24)
|
||||
*/
|
||||
private addSessionPlatformSourceColumn(): void {
|
||||
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(24) as SchemaVersion | undefined;
|
||||
if (applied) return;
|
||||
|
||||
const tableInfo = this.db.query('PRAGMA table_info(sdk_sessions)').all() as TableColumnInfo[];
|
||||
const hasColumn = tableInfo.some(col => col.name === 'platform_source');
|
||||
|
||||
if (!hasColumn) {
|
||||
this.db.run(`ALTER TABLE sdk_sessions ADD COLUMN platform_source TEXT NOT NULL DEFAULT '${DEFAULT_PLATFORM_SOURCE}'`);
|
||||
logger.debug('DB', 'Added platform_source column to sdk_sessions table');
|
||||
}
|
||||
|
||||
this.db.run(`
|
||||
UPDATE sdk_sessions
|
||||
SET platform_source = '${DEFAULT_PLATFORM_SOURCE}'
|
||||
WHERE platform_source IS NULL OR platform_source = ''
|
||||
`);
|
||||
this.db.run('CREATE INDEX IF NOT EXISTS idx_sdk_sessions_platform_source ON sdk_sessions(platform_source)');
|
||||
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(24, new Date().toISOString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the memory session ID for a session
|
||||
* Called by SDKAgent when it captures the session ID from the first SDK message
|
||||
@@ -1002,14 +1040,26 @@ export class SessionStore {
|
||||
subtitle: string | null;
|
||||
text: string;
|
||||
project: string;
|
||||
platform_source: string;
|
||||
prompt_number: number | null;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}> {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT id, type, title, subtitle, text, project, prompt_number, created_at, created_at_epoch
|
||||
FROM observations
|
||||
ORDER BY created_at_epoch DESC
|
||||
SELECT
|
||||
o.id,
|
||||
o.type,
|
||||
o.title,
|
||||
o.subtitle,
|
||||
o.text,
|
||||
o.project,
|
||||
COALESCE(s.platform_source, '${DEFAULT_PLATFORM_SOURCE}') as platform_source,
|
||||
o.prompt_number,
|
||||
o.created_at,
|
||||
o.created_at_epoch
|
||||
FROM observations o
|
||||
LEFT JOIN sdk_sessions s ON o.memory_session_id = s.memory_session_id
|
||||
ORDER BY o.created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
|
||||
@@ -1030,16 +1080,30 @@ export class SessionStore {
|
||||
files_edited: string | null;
|
||||
notes: string | null;
|
||||
project: string;
|
||||
platform_source: string;
|
||||
prompt_number: number | null;
|
||||
created_at: string;
|
||||
created_at_epoch: number;
|
||||
}> {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT id, request, investigated, learned, completed, next_steps,
|
||||
files_read, files_edited, notes, project, prompt_number,
|
||||
created_at, created_at_epoch
|
||||
FROM session_summaries
|
||||
ORDER BY created_at_epoch DESC
|
||||
SELECT
|
||||
ss.id,
|
||||
ss.request,
|
||||
ss.investigated,
|
||||
ss.learned,
|
||||
ss.completed,
|
||||
ss.next_steps,
|
||||
ss.files_read,
|
||||
ss.files_edited,
|
||||
ss.notes,
|
||||
ss.project,
|
||||
COALESCE(s.platform_source, '${DEFAULT_PLATFORM_SOURCE}') as platform_source,
|
||||
ss.prompt_number,
|
||||
ss.created_at,
|
||||
ss.created_at_epoch
|
||||
FROM session_summaries ss
|
||||
LEFT JOIN sdk_sessions s ON ss.memory_session_id = s.memory_session_id
|
||||
ORDER BY ss.created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
|
||||
@@ -1053,6 +1117,7 @@ export class SessionStore {
|
||||
id: number;
|
||||
content_session_id: string;
|
||||
project: string;
|
||||
platform_source: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
created_at: string;
|
||||
@@ -1063,6 +1128,7 @@ export class SessionStore {
|
||||
up.id,
|
||||
up.content_session_id,
|
||||
s.project,
|
||||
COALESCE(s.platform_source, '${DEFAULT_PLATFORM_SOURCE}') as platform_source,
|
||||
up.prompt_number,
|
||||
up.prompt_text,
|
||||
up.created_at,
|
||||
@@ -1079,18 +1145,74 @@ export class SessionStore {
|
||||
/**
|
||||
* Get all unique projects from the database (for web UI project filter)
|
||||
*/
|
||||
getAllProjects(): string[] {
|
||||
const stmt = this.db.prepare(`
|
||||
getAllProjects(platformSource?: string): string[] {
|
||||
const normalizedPlatformSource = platformSource ? normalizePlatformSource(platformSource) : undefined;
|
||||
let query = `
|
||||
SELECT DISTINCT project
|
||||
FROM sdk_sessions
|
||||
WHERE project IS NOT NULL AND project != ''
|
||||
ORDER BY project ASC
|
||||
`);
|
||||
`;
|
||||
const params: unknown[] = [];
|
||||
|
||||
const rows = stmt.all() as Array<{ project: string }>;
|
||||
if (normalizedPlatformSource) {
|
||||
query += ' AND COALESCE(platform_source, ?) = ?';
|
||||
params.push(DEFAULT_PLATFORM_SOURCE, normalizedPlatformSource);
|
||||
}
|
||||
|
||||
query += ' ORDER BY project ASC';
|
||||
|
||||
const rows = this.db.prepare(query).all(...params) as Array<{ project: string }>;
|
||||
return rows.map(row => row.project);
|
||||
}
|
||||
|
||||
getProjectCatalog(): {
|
||||
projects: string[];
|
||||
sources: string[];
|
||||
projectsBySource: Record<string, string[]>;
|
||||
} {
|
||||
const rows = this.db.prepare(`
|
||||
SELECT
|
||||
COALESCE(platform_source, '${DEFAULT_PLATFORM_SOURCE}') as platform_source,
|
||||
project,
|
||||
MAX(started_at_epoch) as latest_epoch
|
||||
FROM sdk_sessions
|
||||
WHERE project IS NOT NULL AND project != ''
|
||||
GROUP BY COALESCE(platform_source, '${DEFAULT_PLATFORM_SOURCE}'), project
|
||||
ORDER BY latest_epoch DESC
|
||||
`).all() as Array<{ platform_source: string; project: string; latest_epoch: number }>;
|
||||
|
||||
const projects: string[] = [];
|
||||
const seenProjects = new Set<string>();
|
||||
const projectsBySource: Record<string, string[]> = {};
|
||||
|
||||
for (const row of rows) {
|
||||
const source = normalizePlatformSource(row.platform_source);
|
||||
|
||||
if (!projectsBySource[source]) {
|
||||
projectsBySource[source] = [];
|
||||
}
|
||||
|
||||
if (!projectsBySource[source].includes(row.project)) {
|
||||
projectsBySource[source].push(row.project);
|
||||
}
|
||||
|
||||
if (!seenProjects.has(row.project)) {
|
||||
seenProjects.add(row.project);
|
||||
projects.push(row.project);
|
||||
}
|
||||
}
|
||||
|
||||
const sources = sortPlatformSources(Object.keys(projectsBySource));
|
||||
|
||||
return {
|
||||
projects,
|
||||
sources,
|
||||
projectsBySource: Object.fromEntries(
|
||||
sources.map(source => [source, projectsBySource[source] || []])
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest user prompt with session info for a Claude session
|
||||
* Used for syncing prompts to Chroma during session initialization
|
||||
@@ -1100,6 +1222,7 @@ export class SessionStore {
|
||||
content_session_id: string;
|
||||
memory_session_id: string;
|
||||
project: string;
|
||||
platform_source: string;
|
||||
prompt_number: number;
|
||||
prompt_text: string;
|
||||
created_at_epoch: number;
|
||||
@@ -1108,7 +1231,8 @@ export class SessionStore {
|
||||
SELECT
|
||||
up.*,
|
||||
s.memory_session_id,
|
||||
s.project
|
||||
s.project,
|
||||
COALESCE(s.platform_source, '${DEFAULT_PLATFORM_SOURCE}') as platform_source
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.content_session_id = s.content_session_id
|
||||
WHERE up.content_session_id = ?
|
||||
@@ -1339,11 +1463,14 @@ export class SessionStore {
|
||||
content_session_id: string;
|
||||
memory_session_id: string | null;
|
||||
project: string;
|
||||
platform_source: string;
|
||||
user_prompt: string;
|
||||
custom_title: string | null;
|
||||
} | null {
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT id, content_session_id, memory_session_id, project, user_prompt, custom_title
|
||||
SELECT id, content_session_id, memory_session_id, project,
|
||||
COALESCE(platform_source, '${DEFAULT_PLATFORM_SOURCE}') as platform_source,
|
||||
user_prompt, custom_title
|
||||
FROM sdk_sessions
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
@@ -1361,6 +1488,7 @@ export class SessionStore {
|
||||
content_session_id: string;
|
||||
memory_session_id: string;
|
||||
project: string;
|
||||
platform_source: string;
|
||||
user_prompt: string;
|
||||
custom_title: string | null;
|
||||
started_at: string;
|
||||
@@ -1373,7 +1501,9 @@ export class SessionStore {
|
||||
|
||||
const placeholders = memorySessionIds.map(() => '?').join(',');
|
||||
const stmt = this.db.prepare(`
|
||||
SELECT id, content_session_id, memory_session_id, project, user_prompt, custom_title,
|
||||
SELECT id, content_session_id, memory_session_id, project,
|
||||
COALESCE(platform_source, '${DEFAULT_PLATFORM_SOURCE}') as platform_source,
|
||||
user_prompt, custom_title,
|
||||
started_at, started_at_epoch, completed_at, completed_at_epoch, status
|
||||
FROM sdk_sessions
|
||||
WHERE memory_session_id IN (${placeholders})
|
||||
@@ -1418,9 +1548,17 @@ export class SessionStore {
|
||||
* Pure get-or-create: never modifies memory_session_id.
|
||||
* Multi-terminal isolation is handled by ON UPDATE CASCADE at the schema level.
|
||||
*/
|
||||
createSDKSession(contentSessionId: string, project: string, userPrompt: string, customTitle?: string): number {
|
||||
createSDKSession(
|
||||
contentSessionId: string,
|
||||
project: string,
|
||||
userPrompt: string,
|
||||
customTitle?: string,
|
||||
platformSource?: string
|
||||
): number {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
const resolved = resolveCreateSessionArgs(customTitle, platformSource);
|
||||
const normalizedPlatformSource = normalizePlatformSource(resolved.platformSource);
|
||||
|
||||
// Session reuse: Return existing session ID if already created for this contentSessionId.
|
||||
const existing = this.db.prepare(`
|
||||
@@ -1436,12 +1574,17 @@ export class SessionStore {
|
||||
`).run(project, contentSessionId);
|
||||
}
|
||||
// Backfill custom_title if provided and not yet set
|
||||
if (customTitle) {
|
||||
if (resolved.customTitle) {
|
||||
this.db.prepare(`
|
||||
UPDATE sdk_sessions SET custom_title = ?
|
||||
WHERE content_session_id = ? AND custom_title IS NULL
|
||||
`).run(customTitle, contentSessionId);
|
||||
`).run(resolved.customTitle, contentSessionId);
|
||||
}
|
||||
this.db.prepare(`
|
||||
UPDATE sdk_sessions SET platform_source = ?
|
||||
WHERE content_session_id = ?
|
||||
AND COALESCE(platform_source, '') != ?
|
||||
`).run(normalizedPlatformSource, contentSessionId, normalizedPlatformSource);
|
||||
return existing.id;
|
||||
}
|
||||
|
||||
@@ -1451,9 +1594,9 @@ export class SessionStore {
|
||||
// must NEVER equal contentSessionId - that would inject memory messages into the user's transcript!
|
||||
this.db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(content_session_id, memory_session_id, project, user_prompt, custom_title, started_at, started_at_epoch, status)
|
||||
VALUES (?, NULL, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(contentSessionId, project, userPrompt, customTitle || null, now.toISOString(), nowEpoch);
|
||||
(content_session_id, memory_session_id, project, platform_source, user_prompt, custom_title, started_at, started_at_epoch, status)
|
||||
VALUES (?, NULL, ?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(contentSessionId, project, normalizedPlatformSource, userPrompt, resolved.customTitle || null, now.toISOString(), nowEpoch);
|
||||
|
||||
// Return new ID
|
||||
const row = this.db.prepare('SELECT id FROM sdk_sessions WHERE content_session_id = ?')
|
||||
@@ -2233,9 +2376,9 @@ export class SessionStore {
|
||||
// Create new manual session
|
||||
const now = new Date();
|
||||
this.db.prepare(`
|
||||
INSERT INTO sdk_sessions (memory_session_id, content_session_id, project, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, 'active')
|
||||
`).run(memorySessionId, contentSessionId, project, now.toISOString(), now.getTime());
|
||||
INSERT INTO sdk_sessions (memory_session_id, content_session_id, project, platform_source, started_at, started_at_epoch, status)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(memorySessionId, contentSessionId, project, DEFAULT_PLATFORM_SOURCE, now.toISOString(), now.getTime());
|
||||
|
||||
logger.info('SESSION', 'Created manual session', { memorySessionId, project });
|
||||
|
||||
@@ -2261,6 +2404,7 @@ export class SessionStore {
|
||||
content_session_id: string;
|
||||
memory_session_id: string;
|
||||
project: string;
|
||||
platform_source?: string;
|
||||
user_prompt: string;
|
||||
started_at: string;
|
||||
started_at_epoch: number;
|
||||
@@ -2279,15 +2423,16 @@ export class SessionStore {
|
||||
|
||||
const stmt = this.db.prepare(`
|
||||
INSERT INTO sdk_sessions (
|
||||
content_session_id, memory_session_id, project, user_prompt,
|
||||
content_session_id, memory_session_id, project, platform_source, user_prompt,
|
||||
started_at, started_at_epoch, completed_at, completed_at_epoch, status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
session.content_session_id,
|
||||
session.memory_session_id,
|
||||
session.project,
|
||||
normalizePlatformSource(session.platform_source),
|
||||
session.user_prompt,
|
||||
session.started_at,
|
||||
session.started_at_epoch,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
TableNameRow,
|
||||
SchemaVersion
|
||||
} from '../../../types/database.js';
|
||||
import { DEFAULT_PLATFORM_SOURCE } from '../../../shared/platform-source.js';
|
||||
|
||||
/**
|
||||
* MigrationRunner handles all database schema migrations
|
||||
@@ -34,6 +35,7 @@ export class MigrationRunner {
|
||||
this.addOnUpdateCascadeToForeignKeys();
|
||||
this.addObservationContentHashColumn();
|
||||
this.addSessionCustomTitleColumn();
|
||||
this.addSessionPlatformSourceColumn();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,6 +63,7 @@ export class MigrationRunner {
|
||||
content_session_id TEXT UNIQUE NOT NULL,
|
||||
memory_session_id TEXT UNIQUE,
|
||||
project TEXT NOT NULL,
|
||||
platform_source TEXT NOT NULL DEFAULT 'claude',
|
||||
user_prompt TEXT,
|
||||
started_at TEXT NOT NULL,
|
||||
started_at_epoch INTEGER NOT NULL,
|
||||
@@ -863,4 +866,29 @@ export class MigrationRunner {
|
||||
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(23, new Date().toISOString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add platform_source column to sdk_sessions for Claude/Codex isolation (migration 24)
|
||||
*/
|
||||
private addSessionPlatformSourceColumn(): void {
|
||||
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(24) as SchemaVersion | undefined;
|
||||
if (applied) return;
|
||||
|
||||
const tableInfo = this.db.query('PRAGMA table_info(sdk_sessions)').all() as TableColumnInfo[];
|
||||
const hasColumn = tableInfo.some(col => col.name === 'platform_source');
|
||||
|
||||
if (!hasColumn) {
|
||||
this.db.run(`ALTER TABLE sdk_sessions ADD COLUMN platform_source TEXT NOT NULL DEFAULT '${DEFAULT_PLATFORM_SOURCE}'`);
|
||||
logger.debug('DB', 'Added platform_source column to sdk_sessions table');
|
||||
}
|
||||
|
||||
this.db.run(`
|
||||
UPDATE sdk_sessions
|
||||
SET platform_source = '${DEFAULT_PLATFORM_SOURCE}'
|
||||
WHERE platform_source IS NULL OR platform_source = ''
|
||||
`);
|
||||
this.db.run('CREATE INDEX IF NOT EXISTS idx_sdk_sessions_platform_source ON sdk_sessions(platform_source)');
|
||||
|
||||
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(24, new Date().toISOString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,17 @@
|
||||
|
||||
import type { Database } from 'bun:sqlite';
|
||||
import { logger } from '../../../utils/logger.js';
|
||||
import { DEFAULT_PLATFORM_SOURCE, normalizePlatformSource } from '../../../shared/platform-source.js';
|
||||
|
||||
function resolveCreateSessionArgs(
|
||||
customTitle?: string,
|
||||
platformSource?: string
|
||||
): { customTitle?: string; platformSource: string } {
|
||||
return {
|
||||
customTitle,
|
||||
platformSource: platformSource ?? DEFAULT_PLATFORM_SOURCE
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new SDK session (idempotent - returns existing session ID if already exists)
|
||||
@@ -22,10 +33,13 @@ export function createSDKSession(
|
||||
contentSessionId: string,
|
||||
project: string,
|
||||
userPrompt: string,
|
||||
customTitle?: string
|
||||
customTitle?: string,
|
||||
platformSource?: string
|
||||
): number {
|
||||
const now = new Date();
|
||||
const nowEpoch = now.getTime();
|
||||
const resolved = resolveCreateSessionArgs(customTitle, platformSource);
|
||||
const normalizedPlatformSource = normalizePlatformSource(resolved.platformSource);
|
||||
|
||||
// Check for existing session
|
||||
const existing = db.prepare(`
|
||||
@@ -41,12 +55,17 @@ export function createSDKSession(
|
||||
`).run(project, contentSessionId);
|
||||
}
|
||||
// Backfill custom_title if provided and not yet set
|
||||
if (customTitle) {
|
||||
if (resolved.customTitle) {
|
||||
db.prepare(`
|
||||
UPDATE sdk_sessions SET custom_title = ?
|
||||
WHERE content_session_id = ? AND custom_title IS NULL
|
||||
`).run(customTitle, contentSessionId);
|
||||
`).run(resolved.customTitle, contentSessionId);
|
||||
}
|
||||
db.prepare(`
|
||||
UPDATE sdk_sessions SET platform_source = ?
|
||||
WHERE content_session_id = ?
|
||||
AND COALESCE(platform_source, '') != ?
|
||||
`).run(normalizedPlatformSource, contentSessionId, normalizedPlatformSource);
|
||||
return existing.id;
|
||||
}
|
||||
|
||||
@@ -56,9 +75,9 @@ export function createSDKSession(
|
||||
// must NEVER equal contentSessionId - that would inject memory messages into the user's transcript!
|
||||
db.prepare(`
|
||||
INSERT INTO sdk_sessions
|
||||
(content_session_id, memory_session_id, project, user_prompt, custom_title, started_at, started_at_epoch, status)
|
||||
VALUES (?, NULL, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(contentSessionId, project, userPrompt, customTitle || null, now.toISOString(), nowEpoch);
|
||||
(content_session_id, memory_session_id, project, platform_source, user_prompt, custom_title, started_at, started_at_epoch, status)
|
||||
VALUES (?, NULL, ?, ?, ?, ?, ?, ?, 'active')
|
||||
`).run(contentSessionId, project, normalizedPlatformSource, userPrompt, resolved.customTitle || null, now.toISOString(), nowEpoch);
|
||||
|
||||
// Return new ID
|
||||
const row = db.prepare('SELECT id FROM sdk_sessions WHERE content_session_id = ?')
|
||||
|
||||
Reference in New Issue
Block a user