Revert "fix: prevent crashes in long-running Electron sessions"

This reverts commit 460e324fe1.
This commit is contained in:
Zane Staggs
2026-02-26 12:26:35 -08:00
parent 3b035182b7
commit 2ad64887f1
5 changed files with 177 additions and 79 deletions
-5
View File
@@ -48,7 +48,6 @@ import StandaloneAppView from './components/apps/StandaloneAppView';
import { View, ViewOptions } from './utils/navigationUtils';
import { useNavigation } from './hooks/useNavigation';
import { evictSessionFromCache } from './hooks/useChatStream';
import { errorMessage } from './utils/conversionUtils';
import { getInitialWorkingDir } from './utils/workingDir';
import { usePageViewTracking } from './hooks/useAnalytics';
@@ -383,10 +382,6 @@ export function AppInner() {
const newSession = { sessionId, initialMessage };
const updated = [...prev, newSession];
if (updated.length > MAX_ACTIVE_SESSIONS) {
const evicted = updated.slice(0, updated.length - MAX_ACTIVE_SESSIONS);
for (const s of evicted) {
evictSessionFromCache(s.sessionId);
}
return updated.slice(updated.length - MAX_ACTIVE_SESSIONS);
}
return updated;
+7 -31
View File
@@ -302,10 +302,7 @@ export const startGoosed = async (options: StartGoosedOptions): Promise<GoosedRe
goosedProcess.stdout?.on('data', (data: Buffer) => {
const text = data.toString();
// Only log small stdout chunks to avoid memory pressure from large payloads
if (data.length < 1024) {
logger.info(`goosed stdout: ${text}`);
}
logger.info(`goosed stdout for port ${port} and dir ${workingDir}: ${text}`);
if (!resolved && text.includes(FINGERPRINT_PREFIX)) {
for (const line of text.split('\n')) {
@@ -328,36 +325,15 @@ export const startGoosed = async (options: StartGoosedOptions): Promise<GoosedRe
});
});
const MAX_ERROR_LOG_LINES = 200;
let stderrBytesProcessed = 0;
const fatalBytes = [
Buffer.from('panicked at'),
Buffer.from('RUST_BACKTRACE'),
Buffer.from('fatal error'),
];
goosedProcess.stderr?.on('data', (data: Buffer) => {
stderrBytesProcessed += data.length;
// Only convert to string if the chunk might contain a fatal error,
// or during startup when we need errorLog for status checks.
const isStartup = stderrBytesProcessed < 1024 * 1024;
const hasFatal = fatalBytes.some((pattern) => data.includes(pattern));
if (isStartup || hasFatal) {
const text = data.toString();
const lines = text.split('\n');
for (const line of lines) {
if (line.trim()) {
errorLog.push(line);
if (isFatalError(line)) {
logger.error(`goosed fatal: ${line}`);
}
const lines = data.toString().split('\n');
for (const line of lines) {
if (line.trim()) {
errorLog.push(line);
if (isFatalError(line)) {
logger.error(`goosed stderr for port ${port} and dir ${workingDir}: ${line}`);
}
}
if (errorLog.length > MAX_ERROR_LOG_LINES * 2) {
errorLog.splice(0, errorLog.length - MAX_ERROR_LOG_LINES);
}
}
});
+3 -36
View File
@@ -27,41 +27,8 @@ import { errorMessage } from '../utils/conversionUtils';
import { showExtensionLoadResults } from '../utils/extensionErrorUtils';
import { maybeHandlePlatformEvent } from '../utils/platform_events';
const MAX_CACHED_SESSIONS = 5;
const MAX_ENTRY_WEIGHT = 200;
const resultsCache = new Map<string, { messages: Message[]; session: Session }>();
function messageWeight(messages: Message[]): number {
let weight = 0;
for (const msg of messages) {
for (const content of msg.content) {
weight += content.type === 'toolResponse' || content.type === 'toolRequest' ? 3 : 1;
}
}
return weight;
}
function resultsCacheSet(key: string, value: { messages: Message[]; session: Session }) {
if (messageWeight(value.messages) > MAX_ENTRY_WEIGHT) {
resultsCache.delete(key);
return;
}
resultsCache.delete(key);
resultsCache.set(key, value);
while (resultsCache.size > MAX_CACHED_SESSIONS) {
const oldest = resultsCache.keys().next().value;
if (oldest !== undefined) resultsCache.delete(oldest);
else break;
}
}
export function evictSessionFromCache(sessionId: string) {
resultsCache.delete(sessionId);
}
interface UseChatStreamProps {
sessionId: string;
onStreamFinish: () => void;
@@ -378,10 +345,10 @@ export function useChatStream({
}, [sessionId]);
useEffect(() => {
if (state.session && state.chatState === ChatState.Idle) {
resultsCacheSet(sessionId, { session: state.session, messages: state.messages });
if (state.session) {
resultsCache.set(sessionId, { session: state.session, messages: state.messages });
}
}, [sessionId, state.session, state.messages, state.chatState]);
}, [sessionId, state.session, state.messages]);
const onFinish = useCallback(
async (error?: string): Promise<void> => {
+37 -7
View File
@@ -1536,30 +1536,31 @@ ipcMain.handle('check-ollama', async () => {
const ps = spawn('ps', ['aux']);
const grep = spawn('grep', ['-iw', '[o]llama']);
const outputChunks: string[] = [];
let output = '';
let errorOutput = '';
// Pipe ps output to grep
ps.stdout.pipe(grep.stdin);
grep.stdout.on('data', (data) => {
outputChunks.push(String(data));
output += data.toString();
});
grep.stderr.on('data', (data) => {
errorOutput += String(data);
errorOutput += data.toString();
});
grep.on('close', (code) => {
const output = outputChunks.join('');
if (code !== null && code !== 0 && code !== 1) {
// grep returns 1 when no matches found
console.error('Error executing grep command:', errorOutput);
return resolve(false);
}
console.log('Raw stdout from ps|grep command:', output);
const trimmedOutput = output.trim();
console.log('Trimmed stdout:', trimmedOutput);
const isRunning = trimmedOutput.length > 0;
resolve(isRunning);
});
@@ -1588,8 +1589,37 @@ ipcMain.handle('check-ollama', async () => {
ipcMain.handle('read-file', async (_event, filePath) => {
try {
const expandedPath = expandTilde(filePath);
const buffer = await fs.readFile(expandedPath);
return { file: buffer.toString('utf8'), filePath: expandedPath, error: null, found: true };
if (process.platform === 'win32') {
const buffer = await fs.readFile(expandedPath);
return { file: buffer.toString('utf8'), filePath: expandedPath, error: null, found: true };
}
// Non-Windows: keep previous behavior via cat for parity
return await new Promise((resolve) => {
const cat = spawn('cat', [expandedPath]);
let output = '';
let errorOutput = '';
cat.stdout.on('data', (data) => {
output += data.toString();
});
cat.stderr.on('data', (data) => {
errorOutput += data.toString();
});
cat.on('close', (code) => {
if (code !== 0) {
resolve({ file: '', filePath: expandedPath, error: errorOutput || null, found: false });
return;
}
resolve({ file: output, filePath: expandedPath, error: null, found: true });
});
cat.on('error', (error) => {
console.error('Error reading file:', error);
resolve({ file: '', filePath: expandedPath, error, found: false });
});
});
} catch (error) {
console.error('Error reading file:', error);
return { file: '', filePath: expandTilde(filePath), error, found: false };
+130
View File
@@ -0,0 +1,130 @@
import { Session } from '../api';
import { getApiUrl } from '../config';
import { errorMessage } from './conversionUtils';
/**
* In-memory cache for session data
* Maps session ID to Session object
*/
const sessionCache = new Map<string, Session>();
/**
* In-flight request tracking to prevent duplicate fetches
* Maps session ID to Promise of Session
*/
const inFlightRequests = new Map<string, Promise<Session>>();
/**
* Load a session from the server using the /agent/resume endpoint
* Implements caching to avoid redundant fetches
*
* @param sessionId - The unique identifier for the session
* @param forceRefresh - If true, bypass cache and fetch fresh data
* @returns Promise resolving to the Session object
* @throws Error if the request fails or session not found
*/
export async function loadSession(sessionId: string, forceRefresh = false): Promise<Session> {
if (!forceRefresh && sessionCache.has(sessionId)) {
return sessionCache.get(sessionId)!;
}
if (inFlightRequests.has(sessionId)) {
return inFlightRequests.get(sessionId)!;
}
const fetchPromise = (async () => {
try {
const url = getApiUrl('/agent/resume');
const secretKey = await window.electron.getSecretKey();
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': secretKey,
},
body: JSON.stringify({
session_id: sessionId,
}),
});
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`Failed to load session: HTTP ${response.status} - ${errorText}`);
}
const session: Session = await response.json();
sessionCache.set(sessionId, session);
return session;
} catch (error) {
throw new Error(
`Error loading session ${sessionId}: ${errorMessage(error, 'Unknown error')}`
);
} finally {
inFlightRequests.delete(sessionId);
}
})();
inFlightRequests.set(sessionId, fetchPromise);
return fetchPromise;
}
/**
* Clear a specific session from the cache
* Useful when a session has been updated and needs to be refetched
*
* @param sessionId - The unique identifier for the session to clear
*/
export function clearSessionCache(sessionId: string): void {
sessionCache.delete(sessionId);
}
/**
* Clear all sessions from the cache
* Useful for logout or when switching contexts
*/
export function clearAllSessionCache(): void {
sessionCache.clear();
}
/**
* Check if a session is currently cached
*
* @param sessionId - The unique identifier for the session
* @returns true if the session is in cache, false otherwise
*/
export function isSessionCached(sessionId: string): boolean {
return sessionCache.has(sessionId);
}
/**
* Get a session from cache without fetching
* Returns undefined if not cached
*
* @param sessionId - The unique identifier for the session
* @returns The cached Session object or undefined
*/
export function getCachedSession(sessionId: string): Session | undefined {
return sessionCache.get(sessionId);
}
/**
* Preload a session into cache
* Useful when you already have session data from another source
*
* @param session - The Session object to cache
*/
export function preloadSession(session: Session): void {
sessionCache.set(session.id, session);
}
/**
* Get the current cache size
* Useful for debugging and monitoring
*
* @returns The number of sessions currently cached
*/
export function getCacheSize(): number {
return sessionCache.size;
}