From 48e99f2c43e36c798fc33cf27f3190197999052d Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Sat, 25 Apr 2026 07:47:28 +0200 Subject: [PATCH] fix(ios-chat): surface project context block write failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ChatController.resetAndStartInProject swallowed the SFTP write of the Scarf-managed AGENTS.md block via `try?` inside `Task.detached`. On failure (permission denied, SFTP error, malformed path) the user saw no feedback while the UI continued claiming the session was project-scoped — but the agent never received the project context, leading to silently degraded chat quality. Replace the `try? + fire-and-forget` with a `Result`-returning detached task. On `.failure`, log the underlying error via `os.Logger` and route it to the existing ACP error banner (`acpError` / `acpErrorHint` / `acpErrorDetails`) with a friendly "Project context not written — agent will proceed without it" payload. Session still starts; only the context-augmentation step is reported as missing. The session-attribution write at the same flow stays fire-and-forget by design — `SessionAttributionService.persist` already logs failures internally, and a missed attribution is purely cosmetic (Dashboard project-badge cosmetics, not chat function). Replaced the comment to make that intent explicit so future readers don't accidentally "fix" it by promoting attribution failures to the chat banner. Co-Authored-By: Claude Opus 4.7 (1M context) --- scarf/Scarf iOS/Chat/ChatView.swift | 50 ++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/scarf/Scarf iOS/Chat/ChatView.swift b/scarf/Scarf iOS/Chat/ChatView.swift index b2d89d5..c5f97e9 100644 --- a/scarf/Scarf iOS/Chat/ChatView.swift +++ b/scarf/Scarf iOS/Chat/ChatView.swift @@ -1,6 +1,7 @@ import SwiftUI import ScarfCore import ScarfIOS +import os // The Chat feature on iOS is gated on `canImport(SQLite3)` because // `RichChatViewModel` reads session history from `HermesDataService` @@ -445,6 +446,11 @@ final class ChatController { private var client: ACPClient? private var eventTask: Task? + private static let logger = Logger( + subsystem: "com.scarf.ios", + category: "ChatController" + ) + init(context: ServerContext) { self.context = context self.vm = RichChatViewModel(context: context) @@ -572,20 +578,36 @@ final class ChatController { vm.reset() currentProjectName = project.name // Write the context block first. Non-fatal on failure — chat - // still starts, just without the managed block; the user sees - // the error via controller.state if it escalates. + // still starts, just without the managed block. We capture the + // failure (rather than swallowing via `try?`) so the user gets + // a yellow banner explaining the agent won't see project context + // for this session, with the underlying error in "Show details". let block = ProjectContextBlock.renderMinimalBlock( projectName: project.name, projectPath: project.path ) let ctx = context - await Task.detached { - try? ProjectContextBlock.writeBlock( - block, - forProjectAt: project.path, - context: ctx - ) + let projectPath = project.path + let writeResult: Result = await Task.detached { + do { + try ProjectContextBlock.writeBlock( + block, + forProjectAt: projectPath, + context: ctx + ) + return .success(()) + } catch { + return .failure(error) + } }.value + if case .failure(let error) = writeResult { + Self.logger.error( + "ProjectContextBlock.writeBlock failed for \(projectPath, privacy: .public): \(error.localizedDescription, privacy: .public)" + ) + vm.acpError = "Project context not written — agent will proceed without it." + vm.acpErrorHint = "Check that the SSH user can write to \(projectPath)/AGENTS.md." + vm.acpErrorDetails = error.localizedDescription + } await start(projectPath: project.path, projectName: project.name) } @@ -648,9 +670,15 @@ final class ChatController { state = .ready // If this was a project-scoped session, record the - // attribution so the Mac's per-project Sessions tab picks - // it up. Best-effort — ACP session creation already won, - // a failed attribution write is cosmetic. + // attribution so Dashboard's Sessions tab can render the + // project badge for it. Best-effort and intentionally fire- + // and-forget — `SessionAttributionService.persist` already + // logs SFTP failures via `os.Logger` (see the + // `Self.logger.error` in `persist`), and a failed write + // here is purely cosmetic: the chat works, only the badge + // is missing until the next reconcile. We deliberately + // don't surface this to the chat banner because it would + // alarm users about a non-issue. if let projectPath { let ctx = context Task.detached {