Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ function nativeModuleStub(): Plugin {
name: 'native-module-stub',
enforce: 'pre',
resolveId(source) {
// Don't stub our native JSONL parser — it's loaded dynamically at runtime
if (source.includes('claude-devtools-native')) return null
if (source.endsWith('.node')) return STUB_ID
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return null
},
Expand Down Expand Up @@ -48,7 +50,8 @@ export default defineConfig({
outDir: 'dist-electron/main',
rollupOptions: {
input: {
index: resolve(__dirname, 'src/main/index.ts')
index: resolve(__dirname, 'src/main/index.ts'),
sessionParseWorker: resolve(__dirname, 'src/main/workers/sessionParseWorker.ts')
},
output: {
// CJS format so bundled deps can use __dirname/require.
Expand Down
4 changes: 4 additions & 0 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { join } from 'path';

import { initializeIpcHandlers, removeIpcHandlers } from './ipc/handlers';
import { getProjectsBasePath, getTodosBasePath } from './utils/pathDecoder';
import { sessionParserPool } from './workers/SessionParserPool';

// Dynamic renderer heap limit — proportional to system RAM so low-end devices
// are not starved. 50% of total RAM, clamped to [2 GB, 4 GB].
Expand Down Expand Up @@ -409,6 +410,9 @@ function shutdownServices(): void {
sshConnectionManager.dispose();
}

// Terminate worker pool
sessionParserPool.terminate();

// Remove IPC handlers
removeIpcHandlers();

Expand Down
107 changes: 94 additions & 13 deletions src/main/ipc/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
type SessionsByIdsOptions,
type SessionsPaginationOptions,
} from '../types';
import { sessionParserPool } from '../workers/SessionParserPool';

import { coercePageLimit, validateProjectId, validateSessionId } from './guards';

Expand All @@ -33,6 +34,9 @@ const logger = createLogger('IPC:sessions');
// Service registry - set via initialize
let registry: ServiceContextRegistry;

// Sessions where native pipeline produced invalid chunks — permanently use JS fallback
const nativeDisabledSessions = new Set<string>();

/**
* Initializes session handlers with service registry.
*/
Expand Down Expand Up @@ -67,6 +71,9 @@ export function removeSessionHandlers(ipcMain: IpcMain): void {
ipcMain.removeHandler('get-session-metrics');
ipcMain.removeHandler('get-waterfall-data');

// Release accumulated per-session state
nativeDisabledSessions.clear();

logger.info('Session handlers removed');
}

Expand Down Expand Up @@ -219,6 +226,7 @@ async function handleGetSessionDetail(

// Check cache first
let sessionDetail = dataCache.get(cacheKey);
let usedNative = false;

if (!sessionDetail) {
const fsType = projectScanner.getFileSystemProvider().type;
Expand All @@ -231,32 +239,105 @@ async function handleGetSessionDetail(
return null;
}

// Parse session messages
const parsedSession = await sessionParser.parseSession(safeProjectId, safeSessionId);

// Resolve subagents
const subagents = await subagentResolver.resolveSubagents(
// Try native Rust pipeline (local filesystem only).
// Rust handles: JSONL read -> classify -> chunk -> tool executions -> semantic steps.
// Returns serde_json::Value with exact TS field names. JS only converts timestamps.
// JS still handles: subagent resolution (requires filesystem provider).
// Use native Rust pipeline for local sessions WITHOUT subagents.
// Sessions with subagents need ProcessLinker + sidechain context which
// only the JS pipeline provides (Review finding #1).
const hasSubagentFiles = await projectScanner.hasSubagents(
safeProjectId,
safeSessionId,
parsedSession.taskCalls,
parsedSession.messages
safeSessionId
);
session.hasSubagents = subagents.length > 0;
if (fsType === 'local' && !hasSubagentFiles && !nativeDisabledSessions.has(cacheKey)) {
try {
const { buildSessionChunksNative } = await import('../utils/nativeJsonl');
const sessionPath = projectScanner.getSessionPath(safeProjectId, safeSessionId);
const nativeResult = buildSessionChunksNative(sessionPath);
// Validate ALL chunks — not just the first. If any chunk has wrong
// shape, fall back to JS pipeline instead of sending bad data to renderer.
const isValidNative =
nativeResult &&
nativeResult.chunks.length > 0 &&
(nativeResult.chunks as Record<string, unknown>[]).every(
(c) =>
c != null &&
typeof c.chunkType === 'string' &&
'rawMessages' in c &&
'startTime' in c &&
'metrics' in c
);
Comment on lines +260 to +270
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Chunk validation is incomplete — missing required BaseChunk fields.

The validation checks chunkType, rawMessages, startTime, and metrics, but per the BaseChunk interface in src/main/types/chunks.ts:136-147, chunks also require id, endTime, and durationMs. Missing these checks could allow malformed chunks through.

🔧 Suggested validation fix
           const isValidNative =
             nativeResult &&
             nativeResult.chunks.length > 0 &&
             (nativeResult.chunks as Record<string, unknown>[]).every(
               (c) =>
                 c != null &&
+                typeof c.id === 'string' &&
                 typeof c.chunkType === 'string' &&
                 'rawMessages' in c &&
                 'startTime' in c &&
+                'endTime' in c &&
+                typeof c.durationMs === 'number' &&
                 'metrics' in c
             );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isValidNative =
nativeResult &&
nativeResult.chunks.length > 0 &&
(nativeResult.chunks as Record<string, unknown>[]).every(
(c) =>
c != null &&
typeof c.chunkType === 'string' &&
'rawMessages' in c &&
'startTime' in c &&
'metrics' in c
);
const isValidNative =
nativeResult &&
nativeResult.chunks.length > 0 &&
(nativeResult.chunks as Record<string, unknown>[]).every(
(c) =>
c != null &&
typeof c.id === 'string' &&
typeof c.chunkType === 'string' &&
'rawMessages' in c &&
'startTime' in c &&
'endTime' in c &&
typeof c.durationMs === 'number' &&
'metrics' in c
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/sessions.ts` around lines 260 - 270, The current isValidNative
validation (checking nativeResult and nativeResult.chunks and each chunk’s
chunkType, rawMessages, startTime, metrics) is incomplete per the BaseChunk
interface; update the predicate in isValidNative to also verify each chunk has
id (string), endTime (number), and durationMs (number) and that rawMessages has
the expected shape (e.g., is an array or object as used elsewhere). Locate the
isValidNative logic and extend the every(...) checks to include typeof c.id ===
'string', typeof c.endTime === 'number', typeof c.durationMs === 'number' and a
sensible check for rawMessages, so malformed BaseChunk objects are rejected at
validation time.


if (isValidNative) {
sessionDetail = {
session,
messages: [],
chunks: nativeResult.chunks as SessionDetail['chunks'],
processes: [],
metrics: nativeResult.metrics as SessionDetail['metrics'],
};
usedNative = true;
} else if (nativeResult) {
// Native produced chunks but they failed validation — permanently
// disable native for this session to avoid repeated failures.
logger.warn(`Native validation failed for ${cacheKey}, disabling native for this session`);
nativeDisabledSessions.add(cacheKey);
}
} catch {
// Native not available — fall through to JS
}
}

// Build session detail with chunks
sessionDetail = chunkBuilder.buildSessionDetail(session, parsedSession.messages, subagents);
// JS fallback pipeline — dispatch to Worker Thread to avoid blocking main process
if (!usedNative) {
try {
sessionDetail = await sessionParserPool.parse({
projectsDir: projectScanner.getProjectsDir(),
sessionPath: projectScanner.getSessionPath(safeProjectId, safeSessionId),
projectId: safeProjectId,
sessionId: safeSessionId,
fsType,
session,
});
} catch (workerError) {
// Worker failed (timeout, crash, etc.) — fall back to inline blocking parse
logger.warn('Worker parse failed, falling back to inline:', workerError);
const parsedSession = await sessionParser.parseSession(safeProjectId, safeSessionId);
const subagents = await subagentResolver.resolveSubagents(
safeProjectId,
safeSessionId,
parsedSession.taskCalls,
parsedSession.messages
);
session.hasSubagents = subagents.length > 0;
sessionDetail = chunkBuilder.buildSessionDetail(
session,
parsedSession.messages,
subagents
);
}
}

// Cache the result
dataCache.set(cacheKey, sessionDetail);
// Cache JS pipeline results only — native results skip cache so any
// rendering failures on the next request will fall back to JS pipeline.
if (sessionDetail && !usedNative) {
dataCache.set(cacheKey, sessionDetail);
}
}

if (!sessionDetail) {
return null;
}
// Strip raw messages before IPC transfer — the renderer never uses them.
// Only chunks (with semantic steps) and process summaries cross the boundary.
// This cuts IPC serialization + renderer heap by ~50-60%.
return {
...sessionDetail,
messages: [],
processes: sessionDetail.processes.map((p) => ({ ...p, messages: [] })),
// Only report native pipeline when Rust actually handled full chunking.
_nativePipeline: usedNative ? Date.now() : false,
};
} catch (error) {
logger.error(`Error in get-session-detail for ${projectId}/${sessionId}:`, error);
Expand Down
Loading
Loading