fix: don't log 'No such file' as warnings on remote reads

The Result-returning readers I added for the v2.0.1 diagnostics surface
were logging EVERY failure, including routine "file doesn't exist" cases
— e.g. skill.yaml files under ~/.hermes/skills/*/ that are optional
metadata, gateway_state.json before Hermes has started, memories/USER.md
on fresh installs.

In practice this meant the Platforms view and similar feature loaders
that walk directories and read optional files now spam the Console with
warnings on every refresh. That's noisier than useful and actively hides
the signal (permission denied, connection failure, sqlite3 missing) we
added the logging to surface.

readFileDataResult now detects the "no such file" case via either:
- TransportError.fileIO(_, "No such file...") from SSHTransport
- NSCocoaErrorDomain code 260 (NSFileNoSuchFileError) from FileManager
- NSPOSIXErrorDomain code 2 (ENOENT)

and suppresses the warning log for those paths. The Result.failure is
still returned, so any caller that cares (Dashboard's banner, Remote
Diagnostics) can still distinguish missing from present-but-unreadable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alan Wizemann
2026-04-20 13:52:28 -07:00
parent 110170d6e9
commit f8069a4481
@@ -1591,11 +1591,36 @@ struct HermesFileService: Sendable {
let data = try transport.readFile(path) let data = try transport.readFile(path)
return .success(data) return .success(data)
} catch { } catch {
// Don't log "No such file" that's a routine, expected case
// for optional files (skill.yaml, gateway_state.json before
// Hermes starts, ~/.hermes/memories/USER.md on fresh installs,
// etc.). The caller still gets the Result.failure so it can
// distinguish missing from present-but-unreadable.
// Log everything else permission denied, connection drops,
// sqlite3 missing since those are actionable diagnostics.
if !Self.isFileNotFound(error) {
Self.logger.warning("readFile(\(path, privacy: .public)) failed: \(error.localizedDescription, privacy: .public)") Self.logger.warning("readFile(\(path, privacy: .public)) failed: \(error.localizedDescription, privacy: .public)")
}
return .failure(error) return .failure(error)
} }
} }
/// `true` iff the error represents "file does not exist" as opposed to
/// a permission / transport / parse failure. Used to suppress routine
/// logging for optional files while still surfacing real problems.
nonisolated private static func isFileNotFound(_ error: Error) -> Bool {
if let transportErr = error as? TransportError,
case .fileIO(_, let underlying) = transportErr {
return underlying.lowercased().contains("no such file")
}
// Cocoa NSFileNoSuchFileError (returned by LocalTransport when
// reading a missing file via FileManager).
let ns = error as NSError
if ns.domain == NSCocoaErrorDomain && ns.code == 260 { return true }
if ns.domain == NSPOSIXErrorDomain && ns.code == 2 { return true } // ENOENT
return false
}
/// Write a UTF-8 text file atomically through the transport. Matches the /// Write a UTF-8 text file atomically through the transport. Matches the
/// old pre-transport behavior (print + swallow on error) because the /// old pre-transport behavior (print + swallow on error) because the
/// callers don't have a UI path for surfacing I/O failures that's /// callers don't have a UI path for surfacing I/O failures that's