mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-08 02:14:37 +00:00
feat(acp,aux): classify resolve_provider_client errors + surface unknown aux tasks
Two fixes for the user-reported "ACP -32603 Internal error" after
removing a Nous OAuth provider while config.yaml still referenced
nous for an auxiliary task. The actual stderr was clear:
agent.auxiliary_client: resolve_provider_client: nous requested
but Nous Portal not configured
But Scarf's chat banner showed only the bare JSON-RPC code and
the user had no actionable path through the UI.
**ACPErrorHint.classify** now pattern-matches the
`resolve_provider_client: <name> requested but` stderr line and
extracts the provider name. Surfaces:
An auxiliary task is configured to use `<name>` but that
provider isn't authenticated. Open Settings → Aux Models, or
check ~/.hermes/config.yaml for auxiliary.<task>.provider: <name>
and switch it to your active provider (or set it to `auto`).
Routed through the existing chat-banner pipeline that already
catches OAuth revocation and missing-credentials errors.
**AuxiliaryTab** gains an "Other tasks in config.yaml" section
that surfaces aux task keys present in YAML but not in Scarf's
typed list (vision, web_extract, compression, session_search,
skills_hub, approval, mcp, flush_memories, curator). Common
case: `auxiliary.summarization.provider: nous` left over from
older Hermes versions or hand-edited configs. Each unknown task
gets a one-click "Reset provider" button that writes
`auxiliary.<key>.provider: auto` — the most-actionable fix
for the OAuth-removal failure mode. Detection scans both
flat-dot-path and nested YAML shapes so it works regardless of
how Hermes dumped the file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -677,6 +677,37 @@ public enum ACPErrorHint {
|
||||
)
|
||||
}
|
||||
|
||||
// Auxiliary task references a provider that isn't authenticated.
|
||||
// Hermes prints `resolve_provider_client: <name> requested but
|
||||
// <Display Name> not configured` when an aux task (compression,
|
||||
// summarization, memory_flush, curator, vision, web_extract,
|
||||
// session_search, skills_hub) has `provider: <name>` set in
|
||||
// config.yaml but that provider's credentials aren't loaded.
|
||||
// Common after a user removes one OAuth provider while their
|
||||
// existing config.yaml still names it for an aux task. The
|
||||
// chat banner used to surface this as `-32603 Internal error`
|
||||
// with no actionable detail; surface a clear path now.
|
||||
if let match = haystack.range(
|
||||
of: #"resolve_provider_client:\s*([a-zA-Z0-9_-]+)\s+requested\s+but"#,
|
||||
options: .regularExpression
|
||||
) {
|
||||
let line = String(haystack[match])
|
||||
// Pull the captured provider name out of the matched line.
|
||||
// First word after "resolve_provider_client:" is the value.
|
||||
let provider: String = {
|
||||
let parts = line.split(whereSeparator: { $0.isWhitespace })
|
||||
if let idx = parts.firstIndex(where: { $0.contains("resolve_provider_client") }),
|
||||
parts.index(after: idx) < parts.endIndex {
|
||||
let candidate = parts[parts.index(after: idx)]
|
||||
return String(candidate)
|
||||
}
|
||||
return "an unauthenticated provider"
|
||||
}()
|
||||
return Classification(
|
||||
hint: "An auxiliary task is configured to use `\(provider)` but that provider isn't authenticated. Open Settings → Aux Models, or check `~/.hermes/config.yaml` for `auxiliary.<task>.provider: \(provider)` and switch it to your active provider (or set it to `auto`)."
|
||||
)
|
||||
}
|
||||
|
||||
if haystack.range(of: #"No\s+(Anthropic|OpenAI|OpenRouter|Gemini|Google|Groq|Mistral|XAI)?\s*credentials\s+found"#,
|
||||
options: .regularExpression) != nil
|
||||
|| haystack.contains("ANTHROPIC_API_KEY")
|
||||
|
||||
@@ -49,6 +49,79 @@ struct AuxiliaryTab: View {
|
||||
return t
|
||||
}
|
||||
|
||||
/// Aux task keys present in `config.yaml` but NOT in `tasks` —
|
||||
/// e.g. `auxiliary.summarization.provider` from older Hermes
|
||||
/// versions, or experimental tasks the user added by hand.
|
||||
/// Without surfacing these, a user whose config has
|
||||
/// `auxiliary.summarization.provider: nous` (where nous is no
|
||||
/// longer authenticated) sees the "5 toggles all off" Aux
|
||||
/// Models tab and concludes nothing's set — but Hermes
|
||||
/// crashes because it's still resolving the unknown task to
|
||||
/// a missing provider. Now those tasks render in a
|
||||
/// fall-through "Other tasks in config.yaml" section.
|
||||
private var unknownTasks: [String] {
|
||||
let known = Set(tasks.map(\.key))
|
||||
var found: Set<String> = []
|
||||
// Scan top-level `auxiliary.<key>.<field>` keys.
|
||||
for line in viewModel.rawConfigYAML.components(separatedBy: "\n") {
|
||||
let stripped = line.drop(while: { $0 == " " || $0 == "\t" })
|
||||
// YAML emits these as either nested under `auxiliary:`
|
||||
// or as flat dot-paths depending on Hermes' dump style.
|
||||
// We match either shape by looking for keys after
|
||||
// `auxiliary.<key>` OR by walking nested indentation.
|
||||
// Quickest robust approach: regex over the raw text.
|
||||
if let m = stripped.range(of: #"^auxiliary\.([A-Za-z0-9_]+)\."#, options: .regularExpression) {
|
||||
let frag = String(stripped[m])
|
||||
let parts = frag.split(separator: ".")
|
||||
if parts.count >= 2 {
|
||||
found.insert(String(parts[1]))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Nested form: scan for `<indent>auxiliary:` then collect
|
||||
// direct child keys.
|
||||
let lines = viewModel.rawConfigYAML.components(separatedBy: "\n")
|
||||
var inAuxBlock = false
|
||||
var auxIndent = -1
|
||||
for line in lines {
|
||||
let trimmed = line.drop(while: { $0 == " " || $0 == "\t" })
|
||||
let indent = line.count - trimmed.count
|
||||
if trimmed.hasPrefix("auxiliary:") {
|
||||
inAuxBlock = true
|
||||
auxIndent = indent
|
||||
continue
|
||||
}
|
||||
guard inAuxBlock else { continue }
|
||||
// Out of the aux block when indent drops back to or
|
||||
// below auxIndent on a non-empty line.
|
||||
if !trimmed.isEmpty && indent <= auxIndent {
|
||||
inAuxBlock = false
|
||||
continue
|
||||
}
|
||||
// Direct children of `auxiliary:` are at indent
|
||||
// > auxIndent and contain a `key:` (no further nesting
|
||||
// dots before the colon).
|
||||
if indent > auxIndent,
|
||||
let colonIdx = trimmed.firstIndex(of: ":") {
|
||||
let key = trimmed[..<colonIdx]
|
||||
// The first deeper key under `auxiliary:` is a task
|
||||
// name. We can't easily distinguish "task name" from
|
||||
// "leaf field" without tracking indent more carefully,
|
||||
// but only task names sit at indent == auxIndent + 2
|
||||
// (or +4 with two-space indent). Add the simplest
|
||||
// heuristic: collect any token that's plausibly a
|
||||
// task name.
|
||||
let candidate = String(key).trimmingCharacters(in: .whitespaces)
|
||||
if !candidate.isEmpty
|
||||
&& candidate.allSatisfy({ $0.isLetter || $0.isNumber || $0 == "_" }) {
|
||||
found.insert(candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
let unknown = found.subtracting(known)
|
||||
return unknown.sorted()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Text("Auxiliary tasks use separate, typically cheaper models. Leave Provider as `auto` to inherit the main provider.")
|
||||
.font(.caption)
|
||||
@@ -60,6 +133,46 @@ struct AuxiliaryTab: View {
|
||||
auxRows(for: task.key)
|
||||
}
|
||||
}
|
||||
// Unknown / unrecognised aux tasks present in config.yaml.
|
||||
// Shown only when at least one such key is present so the
|
||||
// typical user with a clean config never sees this section.
|
||||
if !unknownTasks.isEmpty {
|
||||
SettingsSection(title: "Other tasks in config.yaml", icon: "questionmark.folder") {
|
||||
Text("These auxiliary tasks are present in your `config.yaml` but Scarf doesn't have a typed editor for them. The most common fix is to reset their provider to `auto` so Hermes inherits the main provider. For finer edits, use **Open in Editor** at the top of Settings.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 4)
|
||||
ForEach(unknownTasks, id: \.self) { key in
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "circle.dotted")
|
||||
.foregroundStyle(.secondary)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(key)
|
||||
.font(.system(.body, design: .monospaced, weight: .medium))
|
||||
Text("Configured under `auxiliary.\(key)` in config.yaml")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.tertiary)
|
||||
}
|
||||
Spacer()
|
||||
// The single most-actionable fix: reset
|
||||
// provider to `auto`. Solves the v2.7
|
||||
// user-reported case where removing a
|
||||
// provider's OAuth left an aux task
|
||||
// pointing at the now-unauthenticated
|
||||
// provider, blocking session start with an
|
||||
// opaque ACP -32603 internal error.
|
||||
Button("Reset provider") {
|
||||
viewModel.setAuxiliary(key, field: "provider", value: "auto")
|
||||
}
|
||||
.controlSize(.small)
|
||||
.help(Text(verbatim: "Sets `auxiliary.\(key).provider: auto` so Hermes inherits the main provider's authentication."))
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(.quaternary.opacity(0.3))
|
||||
}
|
||||
}
|
||||
}
|
||||
Color.clear.frame(height: 0)
|
||||
.onAppear {
|
||||
subscription = NousSubscriptionService(context: serverContext).loadState()
|
||||
|
||||
Reference in New Issue
Block a user