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:
huakson
2026-03-24 08:43:56 -03:00
parent e2a230286d
commit 2b60dd2932
46 changed files with 3665 additions and 607 deletions
+13 -1
View File
@@ -11,6 +11,7 @@
import { Request, Response } from 'express';
import { logger } from '../../../utils/logger.js';
import { AppError } from '../../server/ErrorHandler.js';
export abstract class BaseRouteHandler {
/**
@@ -80,7 +81,18 @@ export abstract class BaseRouteHandler {
protected handleError(res: Response, error: Error, context?: string): void {
logger.failure('WORKER', context || 'Request failed', {}, error);
if (!res.headersSent) {
res.status(500).json({ error: error.message });
const statusCode = error instanceof AppError ? error.statusCode : 500;
const response: Record<string, unknown> = { error: error.message };
if (error instanceof AppError && error.code) {
response.code = error.code;
}
if (error instanceof AppError && error.details !== undefined) {
response.details = error.details;
}
res.status(statusCode).json(response);
}
}
}
+20 -19
View File
@@ -66,8 +66,8 @@ export class DataRoutes extends BaseRouteHandler {
* Get paginated observations
*/
private handleGetObservations = this.wrapHandler((req: Request, res: Response): void => {
const { offset, limit, project } = this.parsePaginationParams(req);
const result = this.paginationHelper.getObservations(offset, limit, project);
const { offset, limit, project, platformSource } = this.parsePaginationParams(req);
const result = this.paginationHelper.getObservations(offset, limit, project, platformSource);
res.json(result);
});
@@ -75,8 +75,8 @@ export class DataRoutes extends BaseRouteHandler {
* Get paginated summaries
*/
private handleGetSummaries = this.wrapHandler((req: Request, res: Response): void => {
const { offset, limit, project } = this.parsePaginationParams(req);
const result = this.paginationHelper.getSummaries(offset, limit, project);
const { offset, limit, project, platformSource } = this.parsePaginationParams(req);
const result = this.paginationHelper.getSummaries(offset, limit, project, platformSource);
res.json(result);
});
@@ -84,8 +84,8 @@ export class DataRoutes extends BaseRouteHandler {
* Get paginated user prompts
*/
private handleGetPrompts = this.wrapHandler((req: Request, res: Response): void => {
const { offset, limit, project } = this.parsePaginationParams(req);
const result = this.paginationHelper.getPrompts(offset, limit, project);
const { offset, limit, project, platformSource } = this.parsePaginationParams(req);
const result = this.paginationHelper.getPrompts(offset, limit, project, platformSource);
res.json(result);
});
@@ -256,19 +256,19 @@ export class DataRoutes extends BaseRouteHandler {
* GET /api/projects
*/
private handleGetProjects = this.wrapHandler((req: Request, res: Response): void => {
const db = this.dbManager.getSessionStore().db;
const store = this.dbManager.getSessionStore();
const platformSource = req.query.platformSource as string | undefined;
const rows = db.prepare(`
SELECT DISTINCT project
FROM observations
WHERE project IS NOT NULL
GROUP BY project
ORDER BY MAX(created_at_epoch) DESC
`).all() as Array<{ project: string }>;
if (platformSource) {
res.json({
projects: store.getAllProjects(platformSource),
sources: [platformSource],
projectsBySource: { [platformSource]: store.getAllProjects(platformSource) }
});
return;
}
const projects = rows.map(row => row.project);
res.json({ projects });
res.json(store.getProjectCatalog());
});
/**
@@ -299,12 +299,13 @@ export class DataRoutes extends BaseRouteHandler {
/**
* Parse pagination parameters from request query
*/
private parsePaginationParams(req: Request): { offset: number; limit: number; project?: string } {
private parsePaginationParams(req: Request): { offset: number; limit: number; project?: string; platformSource?: string } {
const offset = parseInt(req.query.offset as string, 10) || 0;
const limit = Math.min(parseInt(req.query.limit as string, 10) || 20, 100); // Max 100
const project = req.query.project as string | undefined;
const platformSource = req.query.platformSource as string | undefined;
return { offset, limit, project };
return { offset, limit, project, platformSource };
}
/**
@@ -167,6 +167,7 @@ export class SearchRoutes extends BaseRouteHandler {
*/
private handleContextPreview = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
const projectName = req.query.project as string;
const platformSource = req.query.platformSource as string | undefined;
if (!projectName) {
this.badRequest(res, 'Project parameter is required');
@@ -183,7 +184,9 @@ export class SearchRoutes extends BaseRouteHandler {
const contextText = await generateContext(
{
session_id: 'preview-' + Date.now(),
cwd: cwd
cwd: cwd,
projects: [projectName],
platform_source: platformSource
},
true // useColors=true for ANSI terminal output
);
@@ -209,6 +212,7 @@ export class SearchRoutes extends BaseRouteHandler {
const projectsParam = (req.query.projects as string) || (req.query.project as string);
const useColors = req.query.colors === 'true';
const full = req.query.full === 'true';
const platformSource = req.query.platformSource as string | undefined;
if (!projectsParam) {
this.badRequest(res, 'Project(s) parameter is required');
@@ -236,7 +240,8 @@ export class SearchRoutes extends BaseRouteHandler {
session_id: 'context-inject-' + Date.now(),
cwd: cwd,
projects: projects,
full
full,
platform_source: platformSource
},
useColors
);
@@ -22,6 +22,8 @@ import { PrivacyCheckValidator } from '../../validation/PrivacyCheckValidator.js
import { SettingsDefaultsManager } from '../../../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../../../shared/paths.js';
import { getProcessBySession, ensureProcessExit } from '../../ProcessRegistry.js';
import { getProjectName } from '../../../../utils/project-name.js';
import { normalizePlatformSource } from '../../../../shared/platform-source.js';
export class SessionRoutes extends BaseRouteHandler {
private completionHandler: SessionCompletionHandler;
@@ -348,6 +350,7 @@ export class SessionRoutes extends BaseRouteHandler {
id: latestPrompt.id,
content_session_id: latestPrompt.content_session_id,
project: latestPrompt.project,
platform_source: latestPrompt.platform_source,
prompt_number: latestPrompt.prompt_number,
prompt_text: latestPrompt.prompt_text,
created_at_epoch: latestPrompt.created_at_epoch
@@ -497,6 +500,8 @@ export class SessionRoutes extends BaseRouteHandler {
*/
private handleObservationsByClaudeId = this.wrapHandler((req: Request, res: Response): void => {
const { contentSessionId, tool_name, tool_input, tool_response, cwd } = req.body;
const platformSource = normalizePlatformSource(req.body.platformSource);
const project = typeof cwd === 'string' && cwd.trim() ? getProjectName(cwd) : '';
if (!contentSessionId) {
return this.badRequest(res, 'Missing contentSessionId');
@@ -531,7 +536,7 @@ export class SessionRoutes extends BaseRouteHandler {
const store = this.dbManager.getSessionStore();
// Get or create session
const sessionDbId = store.createSDKSession(contentSessionId, '', '');
const sessionDbId = store.createSDKSession(contentSessionId, project, '', undefined, platformSource);
const promptNumber = store.getPromptNumberFromUserPrompts(contentSessionId);
// Privacy check: skip if user prompt was entirely private
@@ -595,6 +600,7 @@ export class SessionRoutes extends BaseRouteHandler {
*/
private handleSummarizeByClaudeId = this.wrapHandler((req: Request, res: Response): void => {
const { contentSessionId, last_assistant_message } = req.body;
const platformSource = normalizePlatformSource(req.body.platformSource);
if (!contentSessionId) {
return this.badRequest(res, 'Missing contentSessionId');
@@ -603,7 +609,7 @@ export class SessionRoutes extends BaseRouteHandler {
const store = this.dbManager.getSessionStore();
// Get or create session
const sessionDbId = store.createSDKSession(contentSessionId, '', '');
const sessionDbId = store.createSDKSession(contentSessionId, '', '', undefined, platformSource);
const promptNumber = store.getPromptNumberFromUserPrompts(contentSessionId);
// Privacy check: skip if user prompt was entirely private
@@ -643,6 +649,7 @@ export class SessionRoutes extends BaseRouteHandler {
*/
private handleCompleteByClaudeId = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
const { contentSessionId } = req.body;
const platformSource = normalizePlatformSource(req.body.platformSource);
logger.info('HTTP', '→ POST /api/sessions/complete', { contentSessionId });
@@ -654,7 +661,7 @@ export class SessionRoutes extends BaseRouteHandler {
// Look up sessionDbId from contentSessionId (createSDKSession is idempotent)
// Pass empty strings - we only need the ID lookup, not to create a new session
const sessionDbId = store.createSDKSession(contentSessionId, '', '');
const sessionDbId = store.createSDKSession(contentSessionId, '', '', undefined, platformSource);
// Check if session is in the active sessions map
const activeSession = this.sessionManager.getSession(sessionDbId);
@@ -698,11 +705,13 @@ export class SessionRoutes extends BaseRouteHandler {
// may omit prompt/project in their payload (#838, #1049)
const project = req.body.project || 'unknown';
const prompt = req.body.prompt || '[media prompt]';
const platformSource = req.body.platformSource || 'claude';
const customTitle = req.body.customTitle || undefined;
logger.info('HTTP', 'SessionRoutes: handleSessionInitByClaudeId called', {
contentSessionId,
project,
platformSource,
prompt_length: prompt?.length,
customTitle
});
@@ -715,7 +724,7 @@ export class SessionRoutes extends BaseRouteHandler {
const store = this.dbManager.getSessionStore();
// Step 1: Create/get SDK session (idempotent INSERT OR IGNORE)
const sessionDbId = store.createSDKSession(contentSessionId, project, prompt, customTitle);
const sessionDbId = store.createSDKSession(contentSessionId, project, prompt, customTitle, platformSource);
// Verify session creation with DB lookup
const dbSession = store.getSessionById(sessionDbId);
@@ -76,11 +76,13 @@ export class ViewerRoutes extends BaseRouteHandler {
// Add client to broadcaster
this.sseBroadcaster.addClient(res);
// Send initial_load event with projects list
const allProjects = this.dbManager.getSessionStore().getAllProjects();
// Send initial_load event with project/source catalog
const projectCatalog = this.dbManager.getSessionStore().getProjectCatalog();
this.sseBroadcaster.broadcast({
type: 'initial_load',
projects: allProjects,
projects: projectCatalog.projects,
sources: projectCatalog.sources,
projectsBySource: projectCatalog.projectsBySource,
timestamp: Date.now()
});