mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 02:26:37 +00:00
fix(ios-chat): surface project context block write failures
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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ScarfCore
|
import ScarfCore
|
||||||
import ScarfIOS
|
import ScarfIOS
|
||||||
|
import os
|
||||||
|
|
||||||
// The Chat feature on iOS is gated on `canImport(SQLite3)` because
|
// The Chat feature on iOS is gated on `canImport(SQLite3)` because
|
||||||
// `RichChatViewModel` reads session history from `HermesDataService`
|
// `RichChatViewModel` reads session history from `HermesDataService`
|
||||||
@@ -445,6 +446,11 @@ final class ChatController {
|
|||||||
private var client: ACPClient?
|
private var client: ACPClient?
|
||||||
private var eventTask: Task<Void, Never>?
|
private var eventTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
private static let logger = Logger(
|
||||||
|
subsystem: "com.scarf.ios",
|
||||||
|
category: "ChatController"
|
||||||
|
)
|
||||||
|
|
||||||
init(context: ServerContext) {
|
init(context: ServerContext) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.vm = RichChatViewModel(context: context)
|
self.vm = RichChatViewModel(context: context)
|
||||||
@@ -572,20 +578,36 @@ final class ChatController {
|
|||||||
vm.reset()
|
vm.reset()
|
||||||
currentProjectName = project.name
|
currentProjectName = project.name
|
||||||
// Write the context block first. Non-fatal on failure — chat
|
// Write the context block first. Non-fatal on failure — chat
|
||||||
// still starts, just without the managed block; the user sees
|
// still starts, just without the managed block. We capture the
|
||||||
// the error via controller.state if it escalates.
|
// 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(
|
let block = ProjectContextBlock.renderMinimalBlock(
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
projectPath: project.path
|
projectPath: project.path
|
||||||
)
|
)
|
||||||
let ctx = context
|
let ctx = context
|
||||||
await Task.detached {
|
let projectPath = project.path
|
||||||
try? ProjectContextBlock.writeBlock(
|
let writeResult: Result<Void, Error> = await Task.detached {
|
||||||
block,
|
do {
|
||||||
forProjectAt: project.path,
|
try ProjectContextBlock.writeBlock(
|
||||||
context: ctx
|
block,
|
||||||
)
|
forProjectAt: projectPath,
|
||||||
|
context: ctx
|
||||||
|
)
|
||||||
|
return .success(())
|
||||||
|
} catch {
|
||||||
|
return .failure(error)
|
||||||
|
}
|
||||||
}.value
|
}.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)
|
await start(projectPath: project.path, projectName: project.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,9 +670,15 @@ final class ChatController {
|
|||||||
state = .ready
|
state = .ready
|
||||||
|
|
||||||
// If this was a project-scoped session, record the
|
// If this was a project-scoped session, record the
|
||||||
// attribution so the Mac's per-project Sessions tab picks
|
// attribution so Dashboard's Sessions tab can render the
|
||||||
// it up. Best-effort — ACP session creation already won,
|
// project badge for it. Best-effort and intentionally fire-
|
||||||
// a failed attribution write is cosmetic.
|
// 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 {
|
if let projectPath {
|
||||||
let ctx = context
|
let ctx = context
|
||||||
Task.detached {
|
Task.detached {
|
||||||
|
|||||||
Reference in New Issue
Block a user