From bab91e8b2c64bd3356579d71eaeebd096206dd47 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Mon, 20 Apr 2026 18:03:49 +1000 Subject: [PATCH 1/9] added more logging to debug --- crates/goose-server/src/commands/agent.rs | 16 ++ crates/goose-server/src/main.rs | 34 +++++ ui/desktop/src/goosed.ts | 170 +++++++++++++++++++++- ui/desktop/src/main.ts | 46 +++++- 4 files changed, 257 insertions(+), 9 deletions(-) diff --git a/crates/goose-server/src/commands/agent.rs b/crates/goose-server/src/commands/agent.rs index 26bb09158516..d6ed24b6336d 100644 --- a/crates/goose-server/src/commands/agent.rs +++ b/crates/goose-server/src/commands/agent.rs @@ -9,6 +9,10 @@ use goose_server::tls::setup_tls; use tower_http::cors::{Any, CorsLayer}; use tracing::info; +fn boot_marker(message: &str) { + eprintln!("GOOSED_BOOT: {message}"); +} + #[cfg(unix)] async fn shutdown_signal() { use tokio::signal::unix::{signal, SignalKind}; @@ -35,14 +39,20 @@ pub async fn run() -> Result<()> { #[cfg(feature = "rustls-tls")] let _ = rustls::crypto::ring::default_provider().install_default(); + boot_marker("logging init start"); crate::logging::setup_logging(Some("goosed"))?; + boot_marker("logging init done"); + boot_marker("config load start"); let settings = configuration::Settings::new()?; + boot_marker("config load done"); let secret_key = std::env::var("GOOSE_SERVER__SECRET_KEY") .unwrap_or_else(|_| hex::encode(rand::random::<[u8; 32]>())); + boot_marker("appstate init start"); let app_state = state::AppState::new(settings.tls).await?; + boot_marker("appstate init done"); // Share the server secret with the tunnel manager so it uses the same // key for forwarded requests, without mutating the process environment. @@ -78,11 +88,13 @@ pub async fn run() -> Result<()> { if settings.tls { #[cfg(any(feature = "rustls-tls", feature = "native-tls"))] { + boot_marker("tls setup start"); let tls_setup = setup_tls( settings.tls_cert_path.as_deref(), settings.tls_key_path.as_deref(), ) .await?; + boot_marker("tls setup done"); let handle = Handle::new(); let shutdown_handle = handle.clone(); @@ -92,6 +104,7 @@ pub async fn run() -> Result<()> { }); info!("listening on https://{}", addr); + boot_marker("listening"); #[cfg(feature = "rustls-tls")] axum_server::bind_rustls(addr, tls_setup.config) @@ -114,9 +127,12 @@ pub async fn run() -> Result<()> { ); } } else { + boot_marker("tcp bind start"); let listener = tokio::net::TcpListener::bind(addr).await?; + boot_marker("tcp bind done"); info!("listening on http://{}", addr); + boot_marker("listening"); axum::serve(listener, app) .with_graceful_shutdown(async { shutdown_signal().await }) diff --git a/crates/goose-server/src/main.rs b/crates/goose-server/src/main.rs index 66a203694b7f..bbb12fe9489f 100644 --- a/crates/goose-server/src/main.rs +++ b/crates/goose-server/src/main.rs @@ -9,6 +9,7 @@ mod state; mod tunnel; use std::path::PathBuf; +use std::{backtrace::Backtrace, panic::PanicHookInfo}; use clap::{Parser, Subcommand}; use goose::agents::validate_extensions; @@ -42,9 +43,42 @@ enum Commands { }, } +fn boot_marker(message: &str) { + eprintln!("GOOSED_BOOT: {message}"); +} + +fn install_panic_hook() { + let default_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |panic_info: &PanicHookInfo<'_>| { + let location = panic_info + .location() + .map(|location| format!("{}:{}", location.file(), location.line())) + .unwrap_or_else(|| "unknown".to_string()); + + let payload = panic_info + .payload() + .downcast_ref::<&str>() + .map(|msg| (*msg).to_string()) + .or_else(|| panic_info.payload().downcast_ref::().cloned()) + .unwrap_or_else(|| "unknown panic payload".to_string()); + + eprintln!("GOOSED_BOOT: panic at {location}: {payload}"); + eprintln!("GOOSED_BOOT: backtrace:\n{}", Backtrace::force_capture()); + + default_hook(panic_info); + })); +} + #[tokio::main] async fn main() -> anyhow::Result<()> { + install_panic_hook(); + boot_marker("main entered"); + let cli = Cli::parse(); + boot_marker(&format!( + "command parsed: {:?}", + std::mem::discriminant(&cli.command) + )); match cli.command { Commands::Agent => { diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts index 0dafe833acbe..af25e08ce2d2 100644 --- a/ui/desktop/src/goosed.ts +++ b/ui/desktop/src/goosed.ts @@ -77,24 +77,36 @@ export const findGoosedBinaryPath = (options: FindBinaryOptions = {}): string => ); }; -export const checkServerStatus = async (client: Client, errorLog: string[]): Promise => { +export interface CheckServerStatusOptions { + onEvent?: (name: string, details?: Record) => void; +} + +export const checkServerStatus = async ( + client: Client, + errorLog: string[], + options: CheckServerStatusOptions = {} +): Promise => { const timeout = 10000; const interval = 100; const maxAttempts = Math.ceil(timeout / interval); + options.onEvent?.('healthcheck_start', { timeoutMs: timeout, intervalMs: interval }); for (let attempt = 1; attempt <= maxAttempts; attempt++) { if (errorLog.some(isFatalError)) { + options.onEvent?.('healthcheck_fatal_error', { attempt }); return false; } try { await status({ client, throwOnError: true }); + options.onEvent?.('healthcheck_success', { attempt }); return true; } catch { await new Promise((resolve) => setTimeout(resolve, interval)); } } + options.onEvent?.('healthcheck_timeout', { timeoutMs: timeout }); return false; }; @@ -153,6 +165,30 @@ export interface StartGoosedOptions { isPackaged?: boolean; resourcesPath?: string; logger?: Logger; + diagnosticsDir?: string; +} + +export interface StartupTraceEvent { + name: string; + at: string; + elapsedMs: number; + details?: Record; +} + +export interface StartupDiagnostics { + attemptId: string; + startedAt: string; + goosedPath: string | null; + workingDir: string; + baseUrl: string | null; + pid: number | null; + certFingerprintSeen: boolean; + healthCheckSucceeded: boolean; + childExitCode: number | null; + childExitSignal: NodeJS.Signals | null; + stderrTail: string[]; + stdoutTail: string[]; + events: StartupTraceEvent[]; } export interface GoosedResult { @@ -164,8 +200,95 @@ export interface GoosedResult { cleanup: () => Promise; client: Client; certFingerprint: string | null; + startupDiagnosticsPath: string | null; + getStartupDiagnostics: () => StartupDiagnostics | null; + recordStartupEvent: (name: string, details?: Record) => void; } +const STARTUP_TAIL_LIMIT = 40; + +const appendTail = (target: string[], lines: string[]) => { + target.push(...lines.filter((line) => line.trim())); + if (target.length > STARTUP_TAIL_LIMIT) { + target.splice(0, target.length - STARTUP_TAIL_LIMIT); + } +}; + +const summarizeEnv = (env: Record): Record => { + const summary: Record = {}; + + for (const [key, value] of Object.entries(env)) { + if (value === undefined) { + continue; + } + summary[key] = + key.toLowerCase().includes('secret') || key.toLowerCase().includes('key') + ? '[REDACTED]' + : value; + } + + return summary; +}; + +const createStartupDiagnostics = ( + diagnosticsDir: string | undefined, + workingDir: string, + goosedPath: string | null, + baseUrl: string | null +) => { + if (!diagnosticsDir) { + return null; + } + + fs.mkdirSync(diagnosticsDir, { recursive: true }); + const startedAt = new Date(); + const attemptId = `goosed-startup-${startedAt.toISOString().replaceAll(':', '-')}-${process.pid}.json`; + const diagnosticsPath = path.join(diagnosticsDir, attemptId); + const monotonicStart = Date.now(); + + const diagnostics: StartupDiagnostics = { + attemptId, + startedAt: startedAt.toISOString(), + goosedPath, + workingDir, + baseUrl, + pid: null, + certFingerprintSeen: false, + healthCheckSucceeded: false, + childExitCode: null, + childExitSignal: null, + stderrTail: [], + stdoutTail: [], + events: [], + }; + + const flush = () => { + fs.writeFileSync(diagnosticsPath, `${JSON.stringify(diagnostics, null, 2)}\n`); + }; + + const record = (name: string, details?: Record) => { + if (name === 'healthcheck_success') { + diagnostics.healthCheckSucceeded = true; + } + diagnostics.events.push({ + name, + at: new Date().toISOString(), + elapsedMs: Date.now() - monotonicStart, + ...(details ? { details } : {}), + }); + flush(); + }; + + flush(); + + return { + diagnosticsPath, + diagnostics, + record, + flush, + }; +}; + const goosedClientForUrlAndSecret = (url: string, secret: string): Client => { return createClient( createConfig({ @@ -187,14 +310,17 @@ export const startGoosed = async (options: StartGoosedOptions): Promise startupTrace?.diagnostics ?? null, + recordStartupEvent: (name, details) => startupTrace?.record(name, details), }; } @@ -214,6 +343,7 @@ export const startGoosed = async (options: StartGoosedOptions): Promise startupTrace?.diagnostics ?? null, + recordStartupEvent: (name, details) => startupTrace?.record(name, details), }; } @@ -235,6 +368,9 @@ export const startGoosed = async (options: StartGoosedOptions): Promise = { ...process.env, @@ -271,8 +407,14 @@ export const startGoosed = async (options: StartGoosedOptions): Promise((resolve) => { @@ -282,12 +424,21 @@ export const startGoosed = async (options: StartGoosedOptions): Promise { const text = data.toString(); logger.info(`goosed stdout for port ${port} and dir ${workingDir}: ${text}`); + const lines = text.split('\n').filter((line) => line.trim()); + appendTail(startupTrace?.diagnostics.stdoutTail ?? [], lines); + if (lines.length > 0) { + startupTrace?.record('stdout', { lines }); + } if (!resolved && text.includes(FINGERPRINT_PREFIX)) { for (const line of text.split('\n')) { if (line.startsWith(FINGERPRINT_PREFIX)) { certFingerprint = line.slice(FINGERPRINT_PREFIX.length).trim(); logger.info(`Pinned cert fingerprint: ${certFingerprint}`); + if (startupTrace) { + startupTrace.diagnostics.certFingerprintSeen = true; + startupTrace.record('fingerprint_received', { certFingerprint }); + } resolved = true; resolve(certFingerprint); break; @@ -319,6 +470,11 @@ export const startGoosed = async (options: StartGoosedOptions): Promise { const lines = data.toString().split('\n'); + const nonEmptyLines = lines.filter((line) => line.trim()); + appendTail(startupTrace?.diagnostics.stderrTail ?? [], nonEmptyLines); + if (nonEmptyLines.length > 0) { + startupTrace?.record('stderr', { lines: nonEmptyLines }); + } for (const line of lines) { if (line.trim()) { errorLog.push(line); @@ -332,15 +488,22 @@ export const startGoosed = async (options: StartGoosedOptions): Promise { goosedProcess.stderr?.off('data', onStderrData); + startupTrace?.record('stderr_collection_stopped'); }; - goosedProcess.on('exit', (code) => { + goosedProcess.on('exit', (code, signal) => { logger.info(`goosed process exited with code ${code} for port ${port} and dir ${workingDir}`); + if (startupTrace) { + startupTrace.diagnostics.childExitCode = code; + startupTrace.diagnostics.childExitSignal = signal; + startupTrace.record('child_exit', { code, signal }); + } }); goosedProcess.on('error', (err) => { logger.error(`Failed to start goosed on port ${port} and dir ${workingDir}`, err); errorLog.push(err.message); + startupTrace?.record('spawn_error', { message: err.message, name: err.name }); }); const cleanup = async (): Promise => { @@ -387,5 +550,8 @@ export const startGoosed = async (options: StartGoosedOptions): Promise startupTrace?.diagnostics ?? null, + recordStartupEvent: (name, details) => startupTrace?.record(name, details), }; }; diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 821ae797837e..90dec4ac1a3e 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -61,6 +61,7 @@ function shouldSetupUpdater(): boolean { // Settings management const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json'); +const STARTUP_LOGS_DIR = path.join(app.getPath('userData'), 'logs', 'startup'); function getSettings(): Settings { if (fsSync.existsSync(SETTINGS_FILE)) { @@ -617,6 +618,7 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => { isPackaged: app.isPackaged, resourcesPath: app.isPackaged ? process.resourcesPath : undefined, logger: log, + diagnosticsDir: STARTUP_LOGS_DIR, }); // For locally-spawned goosed, pin using the fingerprint from stdout. @@ -637,6 +639,9 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => { process: goosedProcess, errorLog, stopErrorLogCollection, + startupDiagnosticsPath, + getStartupDiagnostics, + recordStartupEvent, } = goosedResult; const mainWindowState = windowStateKeeper({ @@ -705,9 +710,27 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => { ); goosedClients.set(mainWindow.id, goosedClient); - const serverReady = await checkServerStatus(goosedClient, errorLog); + const serverReady = await checkServerStatus(goosedClient, errorLog, { + onEvent: recordStartupEvent, + }); if (!serverReady) { const isUsingExternalBackend = settings.externalGoosed?.enabled; + const diagnostics = getStartupDiagnostics(); + const stderrTail = diagnostics?.stderrTail ?? []; + const failureDetailParts = [ + errorLog.join('\n'), + diagnostics?.childExitCode !== null || diagnostics?.childExitSignal + ? `Child exit: code=${diagnostics?.childExitCode ?? 'null'} signal=${diagnostics?.childExitSignal ?? 'null'}` + : 'Child exit: unavailable', + diagnostics?.certFingerprintSeen + ? 'TLS fingerprint observed: yes' + : 'TLS fingerprint observed: no', + diagnostics?.healthCheckSucceeded + ? 'Health check observed: yes' + : 'Health check observed: no', + startupDiagnosticsPath ? `Startup diagnostics: ${startupDiagnosticsPath}` : '', + stderrTail.length > 0 ? `Captured startup stderr:\n${stderrTail.join('\n')}` : '', + ].filter(Boolean); if (isUsingExternalBackend) { const response = dialog.showMessageBoxSync({ @@ -730,16 +753,22 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => { return createChat(app, { initialMessage, dir }); } } else { + recordStartupEvent('startup_failure_dialog_shown', { + childStillRunning: Boolean( + goosedProcess && goosedProcess.exitCode === null && !goosedProcess.killed + ), + }); dialog.showMessageBoxSync({ type: 'error', title: 'Goose Failed to Start', message: 'The backend server failed to start.', - detail: errorLog.join('\n'), + detail: failureDetailParts.join('\n\n'), buttons: ['OK'], }); } app.quit(); } + recordStartupEvent('startup_ready'); // errorLog is only needed during startup to detect fatal errors. // Stop collecting stderr to avoid unbounded memory growth over long sessions. @@ -749,11 +778,14 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => { // Nudge the user if mesh is their provider but isn't running. // Delay to let the renderer mount before sending the IPC event. setTimeout(() => { - mesh.checkProviderRunning(goosedClient).then((ok) => { - if (!ok && !mainWindow.isDestroyed()) { - mainWindow.webContents.send('mesh-not-running'); - } - }).catch(() => {}); + mesh + .checkProviderRunning(goosedClient) + .then((ok) => { + if (!ok && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('mesh-not-running'); + } + }) + .catch(() => {}); }, 5000); // Let windowStateKeeper manage the window From 102558671e2e54a8107a7a7f45df6727ced2140f Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Mon, 20 Apr 2026 18:14:48 +1000 Subject: [PATCH 2/9] fixed check --- ui/desktop/package.json | 2 +- ui/desktop/scripts/i18n-check.js | 11 ++++++++++- ui/desktop/src/goosed.ts | 18 +++++++++++------- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/ui/desktop/package.json b/ui/desktop/package.json index 991667e80caf..596068cd5cf3 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -37,7 +37,7 @@ "test:integration": "vitest run --config vitest.integration.config.ts", "test:integration:watch": "vitest --config vitest.integration.config.ts", "test:integration:debug": "DEBUG=1 vitest run --config vitest.integration.config.ts", - "i18n:extract": "formatjs extract 'src/**/*.{ts,tsx}' --out-file src/i18n/messages/en.json --flatten && pnpm run i18n:compile", + "i18n:extract": "formatjs extract 'src/**/*.{ts,tsx}' --ignore '**/*.d.ts' --out-file src/i18n/messages/en.json --flatten && pnpm run i18n:compile", "i18n:check": "node scripts/i18n-check.js", "i18n:compile": "node scripts/i18n-compile.js" }, diff --git a/ui/desktop/scripts/i18n-check.js b/ui/desktop/scripts/i18n-check.js index fc3418a0792a..2c45d62edc2d 100644 --- a/ui/desktop/scripts/i18n-check.js +++ b/ui/desktop/scripts/i18n-check.js @@ -16,7 +16,16 @@ const tmpFile = path.join(os.tmpdir(), 'en.i18n-check.json'); execFileSync( process.execPath, - [formatjs, 'extract', 'src/**/*.{ts,tsx}', '--out-file', tmpFile, '--flatten'], + [ + formatjs, + 'extract', + 'src/**/*.{ts,tsx}', + '--ignore', + '**/*.d.ts', + '--out-file', + tmpFile, + '--flatten', + ], { stdio: 'inherit', cwd: projectDir } ); diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts index af25e08ce2d2..ebb61cc25055 100644 --- a/ui/desktop/src/goosed.ts +++ b/ui/desktop/src/goosed.ts @@ -185,7 +185,7 @@ export interface StartupDiagnostics { certFingerprintSeen: boolean; healthCheckSucceeded: boolean; childExitCode: number | null; - childExitSignal: NodeJS.Signals | null; + childExitSignal: string | null; stderrTail: string[]; stdoutTail: string[]; events: StartupTraceEvent[]; @@ -242,7 +242,7 @@ const createStartupDiagnostics = ( fs.mkdirSync(diagnosticsDir, { recursive: true }); const startedAt = new Date(); - const attemptId = `goosed-startup-${startedAt.toISOString().replaceAll(':', '-')}-${process.pid}.json`; + const attemptId = `goosed-startup-${startedAt.toISOString().replace(/:/g, '-')}-${process.pid}.json`; const diagnosticsPath = path.join(diagnosticsDir, attemptId); const monotonicStart = Date.now(); @@ -368,9 +368,11 @@ export const startGoosed = async (options: StartGoosedOptions): Promise = { ...process.env, @@ -413,8 +415,10 @@ export const startGoosed = async (options: StartGoosedOptions): Promise((resolve) => { From 2c9073e127b128afa6ba779792fc74ea158b3ea1 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Tue, 21 Apr 2026 13:36:38 +1000 Subject: [PATCH 3/9] remove unnessary logs --- ui/desktop/src/goosed.ts | 33 --------------------------------- ui/desktop/src/main.ts | 8 +------- 2 files changed, 1 insertion(+), 40 deletions(-) diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts index ebb61cc25055..d3593af9b58b 100644 --- a/ui/desktop/src/goosed.ts +++ b/ui/desktop/src/goosed.ts @@ -187,7 +187,6 @@ export interface StartupDiagnostics { childExitCode: number | null; childExitSignal: string | null; stderrTail: string[]; - stdoutTail: string[]; events: StartupTraceEvent[]; } @@ -214,22 +213,6 @@ const appendTail = (target: string[], lines: string[]) => { } }; -const summarizeEnv = (env: Record): Record => { - const summary: Record = {}; - - for (const [key, value] of Object.entries(env)) { - if (value === undefined) { - continue; - } - summary[key] = - key.toLowerCase().includes('secret') || key.toLowerCase().includes('key') - ? '[REDACTED]' - : value; - } - - return summary; -}; - const createStartupDiagnostics = ( diagnosticsDir: string | undefined, workingDir: string, @@ -258,7 +241,6 @@ const createStartupDiagnostics = ( childExitCode: null, childExitSignal: null, stderrTail: [], - stdoutTail: [], events: [], }; @@ -320,7 +302,6 @@ export const startGoosed = async (options: StartGoosedOptions): Promise { const text = data.toString(); logger.info(`goosed stdout for port ${port} and dir ${workingDir}: ${text}`); - const lines = text.split('\n').filter((line) => line.trim()); - appendTail(startupTrace?.diagnostics.stdoutTail ?? [], lines); - if (lines.length > 0) { - startupTrace?.record('stdout', { lines }); - } if (!resolved && text.includes(FINGERPRINT_PREFIX)) { for (const line of text.split('\n')) { @@ -476,9 +447,6 @@ export const startGoosed = async (options: StartGoosedOptions): Promise line.trim()); appendTail(startupTrace?.diagnostics.stderrTail ?? [], nonEmptyLines); - if (nonEmptyLines.length > 0) { - startupTrace?.record('stderr', { lines: nonEmptyLines }); - } for (const line of lines) { if (line.trim()) { errorLog.push(line); @@ -492,7 +460,6 @@ export const startGoosed = async (options: StartGoosedOptions): Promise { goosedProcess.stderr?.off('data', onStderrData); - startupTrace?.record('stderr_collection_stopped'); }; goosedProcess.on('exit', (code, signal) => { diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 90dec4ac1a3e..9ab4bd819ca0 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -718,7 +718,6 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => { const diagnostics = getStartupDiagnostics(); const stderrTail = diagnostics?.stderrTail ?? []; const failureDetailParts = [ - errorLog.join('\n'), diagnostics?.childExitCode !== null || diagnostics?.childExitSignal ? `Child exit: code=${diagnostics?.childExitCode ?? 'null'} signal=${diagnostics?.childExitSignal ?? 'null'}` : 'Child exit: unavailable', @@ -729,6 +728,7 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => { ? 'Health check observed: yes' : 'Health check observed: no', startupDiagnosticsPath ? `Startup diagnostics: ${startupDiagnosticsPath}` : '', + errorLog.length > 0 ? `Startup errors:\n${errorLog.join('\n')}` : '', stderrTail.length > 0 ? `Captured startup stderr:\n${stderrTail.join('\n')}` : '', ].filter(Boolean); @@ -753,11 +753,6 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => { return createChat(app, { initialMessage, dir }); } } else { - recordStartupEvent('startup_failure_dialog_shown', { - childStillRunning: Boolean( - goosedProcess && goosedProcess.exitCode === null && !goosedProcess.killed - ), - }); dialog.showMessageBoxSync({ type: 'error', title: 'Goose Failed to Start', @@ -768,7 +763,6 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => { } app.quit(); } - recordStartupEvent('startup_ready'); // errorLog is only needed during startup to detect fatal errors. // Stop collecting stderr to avoid unbounded memory growth over long sessions. From ed7e88951d222113ff8405d1b21f39c9ba65c0cf Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Tue, 21 Apr 2026 14:01:38 +1000 Subject: [PATCH 4/9] init local inference runtime in the route instead of startup --- crates/goose-server/src/routes/local_inference.rs | 10 +++++----- crates/goose-server/src/state.rs | 6 ------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/crates/goose-server/src/routes/local_inference.rs b/crates/goose-server/src/routes/local_inference.rs index 1248dff1f462..20cef21cd6f1 100644 --- a/crates/goose-server/src/routes/local_inference.rs +++ b/crates/goose-server/src/routes/local_inference.rs @@ -20,7 +20,7 @@ use goose::providers::local_inference::{ model_id_from_repo, LocalModelEntry, ModelDownloadStatus as RegistryDownloadStatus, ModelSettings, ShardFile, FEATURED_MODELS, }, - recommend_local_model, + recommend_local_model, InferenceRuntime, }; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -228,9 +228,9 @@ pub async fn sync_featured_models() -> Result { ) )] pub async fn list_local_models( - axum::extract::State(state): axum::extract::State>, + axum::extract::State(_state): axum::extract::State>, ) -> Result>, ErrorResponse> { - let recommended_id = recommend_local_model(&state.inference_runtime); + let recommended_id = recommend_local_model(&InferenceRuntime::get_or_init()); let registry = get_registry() .lock() @@ -352,7 +352,7 @@ pub async fn search_hf_models( ) )] pub async fn get_repo_files( - axum::extract::State(state): axum::extract::State>, + axum::extract::State(_state): axum::extract::State>, Path((author, repo)): Path<(String, String)>, ) -> Result, ErrorResponse> { let repo_id = format!("{}/{}", author, repo); @@ -360,7 +360,7 @@ pub async fn get_repo_files( .await .map_err(|e| ErrorResponse::internal(format!("Failed to fetch repo files: {}", e)))?; - let available_memory = available_inference_memory_bytes(&state.inference_runtime); + let available_memory = available_inference_memory_bytes(&InferenceRuntime::get_or_init()); let recommended_index = hf_models::recommend_variant(&variants, available_memory); let downloaded_quants = { diff --git a/crates/goose-server/src/state.rs b/crates/goose-server/src/state.rs index 73bb1726acad..f877a2292624 100644 --- a/crates/goose-server/src/state.rs +++ b/crates/goose-server/src/state.rs @@ -13,8 +13,6 @@ use crate::session_event_bus::SessionEventBus; use crate::tunnel::TunnelManager; use goose::agents::ExtensionLoadResult; use goose::gateway::manager::GatewayManager; -#[cfg(feature = "local-inference")] -use goose::providers::local_inference::InferenceRuntime; type ExtensionLoadingTasks = Arc>>>>>>>; @@ -27,8 +25,6 @@ pub struct AppState { pub tunnel_manager: Arc, pub gateway_manager: Arc, pub extension_loading_tasks: ExtensionLoadingTasks, - #[cfg(feature = "local-inference")] - pub inference_runtime: Arc, session_buses: Arc>>>, } @@ -47,8 +43,6 @@ impl AppState { tunnel_manager, gateway_manager, extension_loading_tasks: Arc::new(Mutex::new(HashMap::new())), - #[cfg(feature = "local-inference")] - inference_runtime: InferenceRuntime::get_or_init(), session_buses: Arc::new(Mutex::new(HashMap::new())), })) } From 701435b3a4e28bee591f3dfe907ae8768e56765e Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Tue, 21 Apr 2026 15:01:39 +1000 Subject: [PATCH 5/9] added doc --- .../desktop-startup-debugging.md | 73 +++++++++++++++++++ documentation/docs/troubleshooting/index.mdx | 5 ++ 2 files changed, 78 insertions(+) create mode 100644 documentation/docs/troubleshooting/desktop-startup-debugging.md diff --git a/documentation/docs/troubleshooting/desktop-startup-debugging.md b/documentation/docs/troubleshooting/desktop-startup-debugging.md new file mode 100644 index 000000000000..b774683b70ad --- /dev/null +++ b/documentation/docs/troubleshooting/desktop-startup-debugging.md @@ -0,0 +1,73 @@ +--- +title: Debug Desktop Startup Failures +sidebar_label: Debug Desktop Startup Failures +description: Find the desktop startup diagnostics log, understand the key fields, and share the right artifacts when Goose fails to start. +--- + +When goose Desktop fails before the backend becomes ready, the normal server log may be empty or incomplete. In that case, the most useful artifact is the startup diagnostics JSON written by the desktop app. + +## Find the Startup Diagnostics Log + +goose Desktop writes one startup diagnostics file per launch attempt. + +Typical locations: + +- macOS: `~/Library/Application Support/Goose/logs/startup/` +- Windows: `%APPDATA%\Goose\logs\startup\` +- Linux: `~/.config/Goose/logs/startup/` + +The files are named like: + +```text +goosed-startup-2026-04-21T01-24-03.149Z-23416.json +``` + +If several files exist, use the newest one. + +## What To Share + +When reporting a desktop startup failure, share: + +- the newest `goosed-startup-*.json` +- your goose version +- your operating system and version + +For Windows native crashes, also attach the Windows crash report for `goosed.exe` if available. + +Common places to find the Windows crash report: + +- Event Viewer: `Windows Logs` → `Application` +- Reliability Monitor: `View technical details` +- WER files on disk: + - `%LOCALAPPDATA%\Microsoft\Windows\WER\ReportArchive\` + - `%LOCALAPPDATA%\Microsoft\Windows\WER\ReportQueue\` + +Look for a `Report.wer` related to `goosed.exe`. + +If you are filing a GitHub issue or asking for support, this is usually enough: + +- the newest `goosed-startup-*.json` +- your goose version +- your operating system and version +- on Windows, `Report.wer` for `goosed.exe` if Windows created one + +## What The Startup Log Contains + +In most cases, sharing the newest startup log is enough. + +If you want a quick high-level read, focus on these fields: + +- `childExitCode` or `childExitSignal` + Shows whether the backend process exited during startup. +- `certFingerprintSeen` + Shows whether the backend reached the TLS startup stage. +- `healthCheckSucceeded` + Shows whether the desktop app ever observed the backend as ready. +- `stderrTail` + Shows the most recent startup output captured from the backend. +- `events` + Shows the order of major startup steps like process spawn, health check, and child exit. + +## Related Diagnostics + +For session or in-app issues after goose has started, use the normal diagnostics bundle described in [Diagnostics and Reporting](/docs/troubleshooting/diagnostics-and-reporting). diff --git a/documentation/docs/troubleshooting/index.mdx b/documentation/docs/troubleshooting/index.mdx index e5113f552ca8..cb3a626d90c0 100644 --- a/documentation/docs/troubleshooting/index.mdx +++ b/documentation/docs/troubleshooting/index.mdx @@ -20,6 +20,11 @@ import styles from '@site/src/components/Card/styles.module.css'; description="Use built-in diagnostics, report bugs, and request new features. Includes step-by-step guides for generating troubleshooting data." link="/docs/troubleshooting/diagnostics-and-reporting" /> + Date: Tue, 21 Apr 2026 15:41:45 +1000 Subject: [PATCH 6/9] address review comments --- crates/goose-server/src/commands/agent.rs | 8 +------- crates/goose-server/src/routes/local_inference.rs | 10 +++++----- crates/goose-server/src/state.rs | 15 ++++++++++++++- .../troubleshooting/desktop-startup-debugging.md | 4 ++-- documentation/docs/troubleshooting/index.mdx | 2 +- ui/desktop/src/goosed.ts | 6 ++++++ 6 files changed, 29 insertions(+), 16 deletions(-) diff --git a/crates/goose-server/src/commands/agent.rs b/crates/goose-server/src/commands/agent.rs index d6ed24b6336d..03f9ac1d5df8 100644 --- a/crates/goose-server/src/commands/agent.rs +++ b/crates/goose-server/src/commands/agent.rs @@ -39,20 +39,16 @@ pub async fn run() -> Result<()> { #[cfg(feature = "rustls-tls")] let _ = rustls::crypto::ring::default_provider().install_default(); - boot_marker("logging init start"); + boot_marker("main entered"); crate::logging::setup_logging(Some("goosed"))?; - boot_marker("logging init done"); - boot_marker("config load start"); let settings = configuration::Settings::new()?; - boot_marker("config load done"); let secret_key = std::env::var("GOOSE_SERVER__SECRET_KEY") .unwrap_or_else(|_| hex::encode(rand::random::<[u8; 32]>())); boot_marker("appstate init start"); let app_state = state::AppState::new(settings.tls).await?; - boot_marker("appstate init done"); // Share the server secret with the tunnel manager so it uses the same // key for forwarded requests, without mutating the process environment. @@ -94,7 +90,6 @@ pub async fn run() -> Result<()> { settings.tls_key_path.as_deref(), ) .await?; - boot_marker("tls setup done"); let handle = Handle::new(); let shutdown_handle = handle.clone(); @@ -129,7 +124,6 @@ pub async fn run() -> Result<()> { } else { boot_marker("tcp bind start"); let listener = tokio::net::TcpListener::bind(addr).await?; - boot_marker("tcp bind done"); info!("listening on http://{}", addr); boot_marker("listening"); diff --git a/crates/goose-server/src/routes/local_inference.rs b/crates/goose-server/src/routes/local_inference.rs index 20cef21cd6f1..323b905b1374 100644 --- a/crates/goose-server/src/routes/local_inference.rs +++ b/crates/goose-server/src/routes/local_inference.rs @@ -20,7 +20,7 @@ use goose::providers::local_inference::{ model_id_from_repo, LocalModelEntry, ModelDownloadStatus as RegistryDownloadStatus, ModelSettings, ShardFile, FEATURED_MODELS, }, - recommend_local_model, InferenceRuntime, + recommend_local_model, }; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -228,9 +228,9 @@ pub async fn sync_featured_models() -> Result { ) )] pub async fn list_local_models( - axum::extract::State(_state): axum::extract::State>, + axum::extract::State(state): axum::extract::State>, ) -> Result>, ErrorResponse> { - let recommended_id = recommend_local_model(&InferenceRuntime::get_or_init()); + let recommended_id = recommend_local_model(&state.get_inference_runtime()); let registry = get_registry() .lock() @@ -352,7 +352,7 @@ pub async fn search_hf_models( ) )] pub async fn get_repo_files( - axum::extract::State(_state): axum::extract::State>, + axum::extract::State(state): axum::extract::State>, Path((author, repo)): Path<(String, String)>, ) -> Result, ErrorResponse> { let repo_id = format!("{}/{}", author, repo); @@ -360,7 +360,7 @@ pub async fn get_repo_files( .await .map_err(|e| ErrorResponse::internal(format!("Failed to fetch repo files: {}", e)))?; - let available_memory = available_inference_memory_bytes(&InferenceRuntime::get_or_init()); + let available_memory = available_inference_memory_bytes(&state.get_inference_runtime()); let recommended_index = hf_models::recommend_variant(&variants, available_memory); let downloaded_quants = { diff --git a/crates/goose-server/src/state.rs b/crates/goose-server/src/state.rs index f877a2292624..7f15bc0030be 100644 --- a/crates/goose-server/src/state.rs +++ b/crates/goose-server/src/state.rs @@ -5,7 +5,7 @@ use goose::scheduler_trait::SchedulerTrait; use goose::session::SessionManager; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; use tokio::sync::Mutex; use tokio::task::JoinHandle; @@ -13,6 +13,8 @@ use crate::session_event_bus::SessionEventBus; use crate::tunnel::TunnelManager; use goose::agents::ExtensionLoadResult; use goose::gateway::manager::GatewayManager; +#[cfg(feature = "local-inference")] +use goose::providers::local_inference::InferenceRuntime; type ExtensionLoadingTasks = Arc>>>>>>>; @@ -25,6 +27,8 @@ pub struct AppState { pub tunnel_manager: Arc, pub gateway_manager: Arc, pub extension_loading_tasks: ExtensionLoadingTasks, + #[cfg(feature = "local-inference")] + inference_runtime: Arc>>, session_buses: Arc>>>, } @@ -43,10 +47,19 @@ impl AppState { tunnel_manager, gateway_manager, extension_loading_tasks: Arc::new(Mutex::new(HashMap::new())), + #[cfg(feature = "local-inference")] + inference_runtime: Arc::new(OnceLock::new()), session_buses: Arc::new(Mutex::new(HashMap::new())), })) } + #[cfg(feature = "local-inference")] + pub fn get_inference_runtime(&self) -> Arc { + self.inference_runtime + .get_or_init(InferenceRuntime::get_or_init) + .clone() + } + pub async fn set_extension_loading_task( &self, session_id: String, diff --git a/documentation/docs/troubleshooting/desktop-startup-debugging.md b/documentation/docs/troubleshooting/desktop-startup-debugging.md index b774683b70ad..928a8dd4599b 100644 --- a/documentation/docs/troubleshooting/desktop-startup-debugging.md +++ b/documentation/docs/troubleshooting/desktop-startup-debugging.md @@ -1,7 +1,7 @@ --- title: Debug Desktop Startup Failures sidebar_label: Debug Desktop Startup Failures -description: Find the desktop startup diagnostics log, understand the key fields, and share the right artifacts when Goose fails to start. +description: Find the desktop startup diagnostics log, understand the key fields, and share the right artifacts when goose fails to start. --- When goose Desktop fails before the backend becomes ready, the normal server log may be empty or incomplete. In that case, the most useful artifact is the startup diagnostics JSON written by the desktop app. @@ -64,7 +64,7 @@ If you want a quick high-level read, focus on these fields: - `healthCheckSucceeded` Shows whether the desktop app ever observed the backend as ready. - `stderrTail` - Shows the most recent startup output captured from the backend. + Shows the most recent startup output captured from the backend, including major startup stage markers when available. - `events` Shows the order of major startup steps like process spawn, health check, and child exit. diff --git a/documentation/docs/troubleshooting/index.mdx b/documentation/docs/troubleshooting/index.mdx index cb3a626d90c0..2314fd231231 100644 --- a/documentation/docs/troubleshooting/index.mdx +++ b/documentation/docs/troubleshooting/index.mdx @@ -22,7 +22,7 @@ import styles from '@site/src/components/Card/styles.module.css'; /> Date: Tue, 21 Apr 2026 16:00:44 +1000 Subject: [PATCH 7/9] refactor --- ui/desktop/src/goosed.ts | 94 ++------------------ ui/desktop/src/startupDiagnostics.ts | 123 +++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 89 deletions(-) create mode 100644 ui/desktop/src/startupDiagnostics.ts diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts index d2353973f8f7..128ffed08807 100644 --- a/ui/desktop/src/goosed.ts +++ b/ui/desktop/src/goosed.ts @@ -6,6 +6,11 @@ import { createServer } from 'net'; import { Buffer } from 'node:buffer'; import { status } from './api'; import { Client, createClient, createConfig } from './api/client'; +import { + appendTail, + createStartupDiagnostics, + type StartupDiagnostics, +} from './startupDiagnostics'; export interface Logger { info: (...args: unknown[]) => void; @@ -168,28 +173,6 @@ export interface StartGoosedOptions { diagnosticsDir?: string; } -export interface StartupTraceEvent { - name: string; - at: string; - elapsedMs: number; - details?: Record; -} - -export interface StartupDiagnostics { - attemptId: string; - startedAt: string; - goosedPath: string | null; - workingDir: string; - baseUrl: string | null; - pid: number | null; - certFingerprintSeen: boolean; - healthCheckSucceeded: boolean; - childExitCode: number | null; - childExitSignal: string | null; - stderrTail: string[]; - events: StartupTraceEvent[]; -} - export interface GoosedResult { baseUrl: string; workingDir: string; @@ -204,73 +187,6 @@ export interface GoosedResult { recordStartupEvent: (name: string, details?: Record) => void; } -const STARTUP_TAIL_LIMIT = 40; - -const appendTail = (target: string[], lines: string[]) => { - target.push(...lines.filter((line) => line.trim())); - if (target.length > STARTUP_TAIL_LIMIT) { - target.splice(0, target.length - STARTUP_TAIL_LIMIT); - } -}; - -const createStartupDiagnostics = ( - diagnosticsDir: string | undefined, - workingDir: string, - goosedPath: string | null, - baseUrl: string | null -) => { - if (!diagnosticsDir) { - return null; - } - - fs.mkdirSync(diagnosticsDir, { recursive: true }); - const startedAt = new Date(); - const attemptId = `goosed-startup-${startedAt.toISOString().replace(/:/g, '-')}-${process.pid}.json`; - const diagnosticsPath = path.join(diagnosticsDir, attemptId); - const monotonicStart = Date.now(); - - const diagnostics: StartupDiagnostics = { - attemptId, - startedAt: startedAt.toISOString(), - goosedPath, - workingDir, - baseUrl, - pid: null, - certFingerprintSeen: false, - healthCheckSucceeded: false, - childExitCode: null, - childExitSignal: null, - stderrTail: [], - events: [], - }; - - const flush = () => { - fs.writeFileSync(diagnosticsPath, `${JSON.stringify(diagnostics, null, 2)}\n`); - }; - - const record = (name: string, details?: Record) => { - if (name === 'healthcheck_success') { - diagnostics.healthCheckSucceeded = true; - } - diagnostics.events.push({ - name, - at: new Date().toISOString(), - elapsedMs: Date.now() - monotonicStart, - ...(details ? { details } : {}), - }); - flush(); - }; - - flush(); - - return { - diagnosticsPath, - diagnostics, - record, - flush, - }; -}; - const goosedClientForUrlAndSecret = (url: string, secret: string): Client => { return createClient( createConfig({ diff --git a/ui/desktop/src/startupDiagnostics.ts b/ui/desktop/src/startupDiagnostics.ts new file mode 100644 index 000000000000..287be744f4c5 --- /dev/null +++ b/ui/desktop/src/startupDiagnostics.ts @@ -0,0 +1,123 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export interface StartupTraceEvent { + name: string; + at: string; + elapsedMs: number; + details?: Record; +} + +export interface StartupDiagnostics { + attemptId: string; + startedAt: string; + goosedPath: string | null; + workingDir: string; + baseUrl: string | null; + pid: number | null; + certFingerprintSeen: boolean; + healthCheckSucceeded: boolean; + childExitCode: number | null; + childExitSignal: string | null; + stderrTail: string[]; + events: StartupTraceEvent[]; +} + +export interface StartupTrace { + diagnosticsPath: string; + diagnostics: StartupDiagnostics; + record: (name: string, details?: Record) => void; + flush: () => void; +} + +const STARTUP_TAIL_LIMIT = 40; +const STARTUP_LOGS_TO_KEEP = 20; + +export const appendTail = (target: string[], lines: string[]) => { + target.push(...lines.filter((line) => line.trim())); + if (target.length > STARTUP_TAIL_LIMIT) { + target.splice(0, target.length - STARTUP_TAIL_LIMIT); + } +}; + +const cleanupStartupDiagnostics = (diagnosticsDir: string) => { + const startupLogs = fs + .readdirSync(diagnosticsDir, { withFileTypes: true }) + .filter( + (entry) => + entry.isFile() && + entry.name.startsWith('goosed-startup-') && + entry.name.endsWith('.json') + ) + .map((entry) => { + const filePath = path.join(diagnosticsDir, entry.name); + return { + filePath, + modifiedMs: fs.statSync(filePath).mtimeMs, + }; + }) + .sort((a, b) => b.modifiedMs - a.modifiedMs); + + for (const startupLog of startupLogs.slice(STARTUP_LOGS_TO_KEEP)) { + fs.unlinkSync(startupLog.filePath); + } +}; + +export const createStartupDiagnostics = ( + diagnosticsDir: string | undefined, + workingDir: string, + goosedPath: string | null, + baseUrl: string | null +): StartupTrace | null => { + if (!diagnosticsDir) { + return null; + } + + fs.mkdirSync(diagnosticsDir, { recursive: true }); + cleanupStartupDiagnostics(diagnosticsDir); + const startedAt = new Date(); + const attemptId = `goosed-startup-${startedAt.toISOString().replace(/:/g, '-')}-${process.pid}.json`; + const diagnosticsPath = path.join(diagnosticsDir, attemptId); + const monotonicStart = Date.now(); + + const diagnostics: StartupDiagnostics = { + attemptId, + startedAt: startedAt.toISOString(), + goosedPath, + workingDir, + baseUrl, + pid: null, + certFingerprintSeen: false, + healthCheckSucceeded: false, + childExitCode: null, + childExitSignal: null, + stderrTail: [], + events: [], + }; + + const flush = () => { + fs.writeFileSync(diagnosticsPath, `${JSON.stringify(diagnostics, null, 2)}\n`); + }; + + const record = (name: string, details?: Record) => { + if (name === 'healthcheck_success') { + diagnostics.healthCheckSucceeded = true; + } + diagnostics.events.push({ + name, + at: new Date().toISOString(), + elapsedMs: Date.now() - monotonicStart, + ...(details ? { details } : {}), + }); + flush(); + }; + + flush(); + + return { + diagnosticsPath, + diagnostics, + record, + flush, + }; +}; From 6792440ffb3fbc86b06846ca655fde239f9d822b Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Wed, 22 Apr 2026 10:28:11 +1000 Subject: [PATCH 8/9] return error when init local inference rather than panic --- .../src/routes/local_inference.rs | 6 ++++-- crates/goose-server/src/state.rs | 21 +++++++++++++++---- crates/goose/src/providers/local_inference.rs | 13 +++++++----- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/crates/goose-server/src/routes/local_inference.rs b/crates/goose-server/src/routes/local_inference.rs index 323b905b1374..b7a7dfbde6ab 100644 --- a/crates/goose-server/src/routes/local_inference.rs +++ b/crates/goose-server/src/routes/local_inference.rs @@ -230,7 +230,8 @@ pub async fn sync_featured_models() -> Result { pub async fn list_local_models( axum::extract::State(state): axum::extract::State>, ) -> Result>, ErrorResponse> { - let recommended_id = recommend_local_model(&state.get_inference_runtime()); + let runtime = state.get_inference_runtime()?; + let recommended_id = recommend_local_model(&runtime); let registry = get_registry() .lock() @@ -360,7 +361,8 @@ pub async fn get_repo_files( .await .map_err(|e| ErrorResponse::internal(format!("Failed to fetch repo files: {}", e)))?; - let available_memory = available_inference_memory_bytes(&state.get_inference_runtime()); + let runtime = state.get_inference_runtime()?; + let available_memory = available_inference_memory_bytes(&runtime); let recommended_index = hf_models::recommend_variant(&variants, available_memory); let downloaded_quants = { diff --git a/crates/goose-server/src/state.rs b/crates/goose-server/src/state.rs index 7f15bc0030be..3be8934adbce 100644 --- a/crates/goose-server/src/state.rs +++ b/crates/goose-server/src/state.rs @@ -54,10 +54,23 @@ impl AppState { } #[cfg(feature = "local-inference")] - pub fn get_inference_runtime(&self) -> Arc { - self.inference_runtime - .get_or_init(InferenceRuntime::get_or_init) - .clone() + pub fn get_inference_runtime(&self) -> anyhow::Result> { + if let Some(runtime) = self.inference_runtime.get() { + return Ok(runtime.clone()); + } + + let runtime = InferenceRuntime::get_or_init()?; + + // Another thread may win the race to cache the runtime in AppState. + // In that case, return the already-initialized cached runtime. + match self.inference_runtime.set(runtime.clone()) { + Ok(()) => Ok(runtime), + Err(_) => Ok(self + .inference_runtime + .get() + .expect("inference runtime initialized by another thread") + .clone()), + } } pub async fn set_extension_loading_task( diff --git a/crates/goose/src/providers/local_inference.rs b/crates/goose/src/providers/local_inference.rs index 1115c4b2a20f..2f72616d7237 100644 --- a/crates/goose/src/providers/local_inference.rs +++ b/crates/goose/src/providers/local_inference.rs @@ -62,10 +62,10 @@ pub struct InferenceRuntime { static RUNTIME: StdMutex> = StdMutex::new(Weak::new()); impl InferenceRuntime { - pub fn get_or_init() -> Arc { + pub fn get_or_init() -> Result> { let mut guard = RUNTIME.lock().expect("runtime lock poisoned"); if let Some(runtime) = guard.upgrade() { - return runtime; + return Ok(runtime); } // Safety invariant: the Weak::upgrade() check and LlamaBackend::init() // both execute inside this same mutex guard, so there is no window where @@ -80,7 +80,10 @@ impl InferenceRuntime { the mutex guard prevents concurrent re-init" ) } - Err(e) => panic!("Failed to init llama backend: {}", e), + Err(e) => { + tracing::error!(error = %e, "failed to initialize local inference runtime"); + return Err(anyhow::anyhow!("Failed to init llama backend: {}", e)); + } }; llama_cpp_2::send_logs_to_tracing(LogOptions::default()); let runtime = Arc::new(Self { @@ -88,7 +91,7 @@ impl InferenceRuntime { backend, }); *guard = Arc::downgrade(&runtime); - runtime + Ok(runtime) } pub fn backend(&self) -> &LlamaBackend { @@ -357,7 +360,7 @@ pub struct LocalInferenceProvider { impl LocalInferenceProvider { pub async fn from_env(model: ModelConfig, _extensions: Vec) -> Result { - let runtime = InferenceRuntime::get_or_init(); + let runtime = InferenceRuntime::get_or_init()?; let model_slot = runtime.get_or_create_model_slot(&model.model_name); Ok(Self { runtime, From 29e578eaec7d81883c23c08fd486882790e4bef8 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Wed, 22 Apr 2026 10:38:23 +1000 Subject: [PATCH 9/9] frontend improvement --- ui/desktop/src/goosed.ts | 2 +- ui/desktop/src/startupDiagnostics.ts | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts index 128ffed08807..fcb0604a2951 100644 --- a/ui/desktop/src/goosed.ts +++ b/ui/desktop/src/goosed.ts @@ -213,7 +213,7 @@ export const startGoosed = async (options: StartGoosedOptions): Promise void; } -const STARTUP_TAIL_LIMIT = 40; +const STARTUP_TAIL_LIMIT = 80; const STARTUP_LOGS_TO_KEEP = 20; export const appendTail = (target: string[], lines: string[]) => { @@ -45,9 +45,7 @@ const cleanupStartupDiagnostics = (diagnosticsDir: string) => { .readdirSync(diagnosticsDir, { withFileTypes: true }) .filter( (entry) => - entry.isFile() && - entry.name.startsWith('goosed-startup-') && - entry.name.endsWith('.json') + entry.isFile() && entry.name.startsWith('goosed-startup-') && entry.name.endsWith('.json') ) .map((entry) => { const filePath = path.join(diagnosticsDir, entry.name); @@ -65,9 +63,7 @@ const cleanupStartupDiagnostics = (diagnosticsDir: string) => { export const createStartupDiagnostics = ( diagnosticsDir: string | undefined, - workingDir: string, - goosedPath: string | null, - baseUrl: string | null + workingDir: string ): StartupTrace | null => { if (!diagnosticsDir) { return null; @@ -83,9 +79,9 @@ export const createStartupDiagnostics = ( const diagnostics: StartupDiagnostics = { attemptId, startedAt: startedAt.toISOString(), - goosedPath, + goosedPath: null, workingDir, - baseUrl, + baseUrl: null, pid: null, certFingerprintSeen: false, healthCheckSucceeded: false,