mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Reapply "fix: prevent crashes in long-running Electron sessions"
This reverts commit 2ad64887f1.
This commit is contained in:
@@ -48,6 +48,7 @@ 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';
|
||||
@@ -382,6 +383,10 @@ 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;
|
||||
|
||||
@@ -302,7 +302,10 @@ export const startGoosed = async (options: StartGoosedOptions): Promise<GoosedRe
|
||||
|
||||
goosedProcess.stdout?.on('data', (data: Buffer) => {
|
||||
const text = data.toString();
|
||||
logger.info(`goosed stdout for port ${port} and dir ${workingDir}: ${text}`);
|
||||
// Only log small stdout chunks to avoid memory pressure from large payloads
|
||||
if (data.length < 1024) {
|
||||
logger.info(`goosed stdout: ${text}`);
|
||||
}
|
||||
|
||||
if (!resolved && text.includes(FINGERPRINT_PREFIX)) {
|
||||
for (const line of text.split('\n')) {
|
||||
@@ -325,15 +328,36 @@ 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) => {
|
||||
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}`);
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errorLog.length > MAX_ERROR_LOG_LINES * 2) {
|
||||
errorLog.splice(0, errorLog.length - MAX_ERROR_LOG_LINES);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -27,8 +27,41 @@ 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;
|
||||
@@ -345,10 +378,10 @@ export function useChatStream({
|
||||
}, [sessionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.session) {
|
||||
resultsCache.set(sessionId, { session: state.session, messages: state.messages });
|
||||
if (state.session && state.chatState === ChatState.Idle) {
|
||||
resultsCacheSet(sessionId, { session: state.session, messages: state.messages });
|
||||
}
|
||||
}, [sessionId, state.session, state.messages]);
|
||||
}, [sessionId, state.session, state.messages, state.chatState]);
|
||||
|
||||
const onFinish = useCallback(
|
||||
async (error?: string): Promise<void> => {
|
||||
|
||||
+7
-37
@@ -1536,31 +1536,30 @@ ipcMain.handle('check-ollama', async () => {
|
||||
const ps = spawn('ps', ['aux']);
|
||||
const grep = spawn('grep', ['-iw', '[o]llama']);
|
||||
|
||||
let output = '';
|
||||
const outputChunks: string[] = [];
|
||||
let errorOutput = '';
|
||||
|
||||
// Pipe ps output to grep
|
||||
ps.stdout.pipe(grep.stdin);
|
||||
|
||||
grep.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
outputChunks.push(String(data));
|
||||
});
|
||||
|
||||
grep.stderr.on('data', (data) => {
|
||||
errorOutput += data.toString();
|
||||
errorOutput += String(data);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
@@ -1589,37 +1588,8 @@ ipcMain.handle('check-ollama', async () => {
|
||||
ipcMain.handle('read-file', async (_event, filePath) => {
|
||||
try {
|
||||
const expandedPath = expandTilde(filePath);
|
||||
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 });
|
||||
});
|
||||
});
|
||||
const buffer = await fs.readFile(expandedPath);
|
||||
return { file: buffer.toString('utf8'), filePath: expandedPath, error: null, found: true };
|
||||
} catch (error) {
|
||||
console.error('Error reading file:', error);
|
||||
return { file: '', filePath: expandTilde(filePath), error, found: false };
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
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