From f7fc36dfa92a104a24960ae82dd55b13b2a74644 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:50:34 +0000 Subject: [PATCH 1/2] fix(daemon): log host disk-full side-effect failures at warn level A full disk on the user's machine (io ErrorKind::StorageFull, ENOSPC) made the side-effect failure sites log at ERROR level, so the SentryLayer reported it as an AutterError exception. Add is_storage_full_error and log these failures at warn level instead. The daemon still records the side-effect error. Co-Authored-By: Claude Opus 5.5 Generated-By: PostHog Desktop Task-Id: 5ce83de7-5076-4c53-ba66-d8252b6f4349 --- src/daemon.rs | 73 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 6adb325..60b2b02 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -358,6 +358,14 @@ fn is_missing_working_dir_error(error: &AutterError) -> bool { } } +/// Returns true when the error comes from a full disk on the host (ENOSPC on +/// Unix, ERROR_DISK_FULL on Windows). That is an environment condition on the +/// user's machine, not a daemon fault, so side-effect failures with this cause +/// log at warn level and do not become error-tracking exceptions. +fn is_storage_full_error(error: &AutterError) -> bool { + matches!(error, AutterError::IoError(io_error) if io_error.kind() == std::io::ErrorKind::StorageFull) +} + fn trace_root_sid(sid: &str) -> &str { sid.split('/').next().unwrap_or(sid) } @@ -6001,12 +6009,21 @@ impl ActorDaemonCoordinator { Ok(Ok((applied, side_effect_result))) => { if let Err(error) = &side_effect_result { let _ = self.record_side_effect_error(family, order, error); - tracing::error!( - %error, - %family, - seq = applied.seq, - "command side effect failed" - ); + if is_storage_full_error(error) { + tracing::warn!( + %error, + %family, + seq = applied.seq, + "command side effect failed: host disk is full" + ); + } else { + tracing::error!( + %error, + %family, + seq = applied.seq, + "command side effect failed" + ); + } } if let Err(error) = self.append_command_completion_log( family, @@ -7768,12 +7785,21 @@ impl ActorDaemonCoordinator { let _ = self.end_family_effect(&family); if let Err(error) = result { let _ = self.record_side_effect_error(&family, applied.seq, &error); - tracing::error!( - %error, - %family, - seq = applied.seq, - "async side-effect error" - ); + if is_storage_full_error(&error) { + tracing::warn!( + %error, + %family, + seq = applied.seq, + "async side-effect error: host disk is full" + ); + } else { + tracing::error!( + %error, + %family, + seq = applied.seq, + "async side-effect error" + ); + } } else if let Err(error) = self.append_command_completion_log(&family, &applied, &Ok(()), applied.seq) { @@ -9351,6 +9377,29 @@ mod tests { ))); } + #[test] + fn storage_full_error_detects_disk_full_io_errors() { + let disk_full = AutterError::IoError(std::io::Error::new( + std::io::ErrorKind::StorageFull, + "No space left on device (os error 28)", + )); + assert!(is_storage_full_error(&disk_full)); + + #[cfg(unix)] + assert!(is_storage_full_error(&AutterError::IoError( + std::io::Error::from_raw_os_error(28) + ))); + + let permission_denied = AutterError::IoError(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "access denied", + )); + assert!(!is_storage_full_error(&permission_denied)); + assert!(!is_storage_full_error(&AutterError::Generic( + "No space left on device".to_string() + ))); + } + struct EnvVarGuard { key: &'static str, original: Option, From ae37fe834af676cc0adb147954e71a9711f14d77 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 18:52:11 +0000 Subject: [PATCH 2/2] refactor(daemon): share one log helper for side-effect failures Both side-effect failure sites had the same warn-or-error block. Move it into log_side_effect_error. SentryLayer reads only the event level, so the change does not affect error grouping. Co-Authored-By: Claude Opus 5.5 Generated-By: PostHog Desktop Task-Id: 5ce83de7-5076-4c53-ba66-d8252b6f4349 --- src/daemon.rs | 50 ++++++++++++++++++++------------------------------ 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 60b2b02..bda2c44 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -366,6 +366,14 @@ fn is_storage_full_error(error: &AutterError) -> bool { matches!(error, AutterError::IoError(io_error) if io_error.kind() == std::io::ErrorKind::StorageFull) } +fn log_side_effect_error(error: &AutterError, family: &str, seq: u64, message: &str) { + if is_storage_full_error(error) { + tracing::warn!(%error, %family, seq, "{message}: host disk is full"); + } else { + tracing::error!(%error, %family, seq, "{message}"); + } +} + fn trace_root_sid(sid: &str) -> &str { sid.split('/').next().unwrap_or(sid) } @@ -6009,21 +6017,12 @@ impl ActorDaemonCoordinator { Ok(Ok((applied, side_effect_result))) => { if let Err(error) = &side_effect_result { let _ = self.record_side_effect_error(family, order, error); - if is_storage_full_error(error) { - tracing::warn!( - %error, - %family, - seq = applied.seq, - "command side effect failed: host disk is full" - ); - } else { - tracing::error!( - %error, - %family, - seq = applied.seq, - "command side effect failed" - ); - } + log_side_effect_error( + error, + family, + applied.seq, + "command side effect failed", + ); } if let Err(error) = self.append_command_completion_log( family, @@ -7785,21 +7784,12 @@ impl ActorDaemonCoordinator { let _ = self.end_family_effect(&family); if let Err(error) = result { let _ = self.record_side_effect_error(&family, applied.seq, &error); - if is_storage_full_error(&error) { - tracing::warn!( - %error, - %family, - seq = applied.seq, - "async side-effect error: host disk is full" - ); - } else { - tracing::error!( - %error, - %family, - seq = applied.seq, - "async side-effect error" - ); - } + log_side_effect_error( + &error, + &family, + applied.seq, + "async side-effect error", + ); } else if let Err(error) = self.append_command_completion_log(&family, &applied, &Ok(()), applied.seq) {