Revert "Reapply "fix: prevent crashes in long-running Electron sessions""
This reverts commit 9d0897ac45.
This commit is contained in:
@@ -48,7 +48,6 @@ import StandaloneAppView from './components/apps/StandaloneAppView';
|
|||||||
import { View, ViewOptions } from './utils/navigationUtils';
|
import { View, ViewOptions } from './utils/navigationUtils';
|
||||||
|
|
||||||
import { useNavigation } from './hooks/useNavigation';
|
import { useNavigation } from './hooks/useNavigation';
|
||||||
import { evictSessionFromCache } from './hooks/useChatStream';
|
|
||||||
import { errorMessage } from './utils/conversionUtils';
|
import { errorMessage } from './utils/conversionUtils';
|
||||||
import { getInitialWorkingDir } from './utils/workingDir';
|
import { getInitialWorkingDir } from './utils/workingDir';
|
||||||
import { usePageViewTracking } from './hooks/useAnalytics';
|
import { usePageViewTracking } from './hooks/useAnalytics';
|
||||||
@@ -383,10 +382,6 @@ export function AppInner() {
|
|||||||
const newSession = { sessionId, initialMessage };
|
const newSession = { sessionId, initialMessage };
|
||||||
const updated = [...prev, newSession];
|
const updated = [...prev, newSession];
|
||||||
if (updated.length > MAX_ACTIVE_SESSIONS) {
|
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.slice(updated.length - MAX_ACTIVE_SESSIONS);
|
||||||
}
|
}
|
||||||
return updated;
|
return updated;
|
||||||
|
|||||||
@@ -302,10 +302,7 @@ export const startGoosed = async (options: StartGoosedOptions): Promise<GoosedRe
|
|||||||
|
|
||||||
goosedProcess.stdout?.on('data', (data: Buffer) => {
|
goosedProcess.stdout?.on('data', (data: Buffer) => {
|
||||||
const text = data.toString();
|
const text = data.toString();
|
||||||
// Only log small stdout chunks to avoid memory pressure from large payloads
|
logger.info(`goosed stdout for port ${port} and dir ${workingDir}: ${text}`);
|
||||||
if (data.length < 1024) {
|
|
||||||
logger.info(`goosed stdout: ${text}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resolved && text.includes(FINGERPRINT_PREFIX)) {
|
if (!resolved && text.includes(FINGERPRINT_PREFIX)) {
|
||||||
for (const line of text.split('\n')) {
|
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) => {
|
goosedProcess.stderr?.on('data', (data: Buffer) => {
|
||||||
stderrBytesProcessed += data.length;
|
const lines = data.toString().split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
// Only convert to string if the chunk might contain a fatal error,
|
if (line.trim()) {
|
||||||
// or during startup when we need errorLog for status checks.
|
errorLog.push(line);
|
||||||
const isStartup = stderrBytesProcessed < 1024 * 1024;
|
if (isFatalError(line)) {
|
||||||
const hasFatal = fatalBytes.some((pattern) => data.includes(pattern));
|
logger.error(`goosed stderr for port ${port} and dir ${workingDir}: ${line}`);
|
||||||
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (errorLog.length > MAX_ERROR_LOG_LINES * 2) {
|
|
||||||
errorLog.splice(0, errorLog.length - MAX_ERROR_LOG_LINES);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -27,41 +27,8 @@ import { errorMessage } from '../utils/conversionUtils';
|
|||||||
import { showExtensionLoadResults } from '../utils/extensionErrorUtils';
|
import { showExtensionLoadResults } from '../utils/extensionErrorUtils';
|
||||||
import { maybeHandlePlatformEvent } from '../utils/platform_events';
|
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 }>();
|
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 {
|
interface UseChatStreamProps {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
onStreamFinish: () => void;
|
onStreamFinish: () => void;
|
||||||
@@ -378,10 +345,10 @@ export function useChatStream({
|
|||||||
}, [sessionId]);
|
}, [sessionId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.session && state.chatState === ChatState.Idle) {
|
if (state.session) {
|
||||||
resultsCacheSet(sessionId, { session: state.session, messages: state.messages });
|
resultsCache.set(sessionId, { session: state.session, messages: state.messages });
|
||||||
}
|
}
|
||||||
}, [sessionId, state.session, state.messages, state.chatState]);
|
}, [sessionId, state.session, state.messages]);
|
||||||
|
|
||||||
const onFinish = useCallback(
|
const onFinish = useCallback(
|
||||||
async (error?: string): Promise<void> => {
|
async (error?: string): Promise<void> => {
|
||||||
|
|||||||
+37
-7
@@ -1536,30 +1536,31 @@ ipcMain.handle('check-ollama', async () => {
|
|||||||
const ps = spawn('ps', ['aux']);
|
const ps = spawn('ps', ['aux']);
|
||||||
const grep = spawn('grep', ['-iw', '[o]llama']);
|
const grep = spawn('grep', ['-iw', '[o]llama']);
|
||||||
|
|
||||||
const outputChunks: string[] = [];
|
let output = '';
|
||||||
let errorOutput = '';
|
let errorOutput = '';
|
||||||
|
|
||||||
// Pipe ps output to grep
|
// Pipe ps output to grep
|
||||||
ps.stdout.pipe(grep.stdin);
|
ps.stdout.pipe(grep.stdin);
|
||||||
|
|
||||||
grep.stdout.on('data', (data) => {
|
grep.stdout.on('data', (data) => {
|
||||||
outputChunks.push(String(data));
|
output += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
grep.stderr.on('data', (data) => {
|
grep.stderr.on('data', (data) => {
|
||||||
errorOutput += String(data);
|
errorOutput += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
grep.on('close', (code) => {
|
grep.on('close', (code) => {
|
||||||
const output = outputChunks.join('');
|
|
||||||
|
|
||||||
if (code !== null && code !== 0 && code !== 1) {
|
if (code !== null && code !== 0 && code !== 1) {
|
||||||
// grep returns 1 when no matches found
|
// grep returns 1 when no matches found
|
||||||
console.error('Error executing grep command:', errorOutput);
|
console.error('Error executing grep command:', errorOutput);
|
||||||
return resolve(false);
|
return resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Raw stdout from ps|grep command:', output);
|
||||||
const trimmedOutput = output.trim();
|
const trimmedOutput = output.trim();
|
||||||
|
console.log('Trimmed stdout:', trimmedOutput);
|
||||||
|
|
||||||
const isRunning = trimmedOutput.length > 0;
|
const isRunning = trimmedOutput.length > 0;
|
||||||
resolve(isRunning);
|
resolve(isRunning);
|
||||||
});
|
});
|
||||||
@@ -1588,8 +1589,37 @@ ipcMain.handle('check-ollama', async () => {
|
|||||||
ipcMain.handle('read-file', async (_event, filePath) => {
|
ipcMain.handle('read-file', async (_event, filePath) => {
|
||||||
try {
|
try {
|
||||||
const expandedPath = expandTilde(filePath);
|
const expandedPath = expandTilde(filePath);
|
||||||
const buffer = await fs.readFile(expandedPath);
|
if (process.platform === 'win32') {
|
||||||
return { file: buffer.toString('utf8'), filePath: expandedPath, error: null, found: true };
|
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) {
|
} catch (error) {
|
||||||
console.error('Error reading file:', error);
|
console.error('Error reading file:', error);
|
||||||
return { file: '', filePath: expandTilde(filePath), error, found: false };
|
return { file: '', filePath: expandTilde(filePath), error, found: false };
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user