Summarize: LAN primary with local-Ollama rescue + in-app failure guidance

- Engine: when the primary summarizer is out of retries (or misconfigured),
  run_summarize now finishes the job on the configured rescue provider
  (SHONAR_LLM_FALLBACK_*), tags the summary with the provider that wrote it,
  and stores a human note in job.error; success clears stale notes.
- App (auto/lan): passes Ollama as the rescue provider when it is up.
- Detail screen: shows the rescue-swap note in plain words, a red
  'Summary failed' line with Settings -> Summarizer fix instructions and a
  Retry summary button on hard failure.
- Settings copy explains the fallback. 3 new pytest cases (14/14 pass);
  live E2E on 2026-09-18: sarcastic summary v6 via LAN on attempt 2.
This commit is contained in:
avi 2026-09-17 21:44:02 -05:00
commit 3061629dc5
6 changed files with 172 additions and 8 deletions

View file

@ -358,6 +358,15 @@ class DesktopState(private val appDir: File = defaultAppDir()) {
env["SHONAR_LLM_BASE_URL"] = LAN_LLM_BASE_URL
env["SHONAR_LLM_MODEL"] = LAN_LLM_MODEL
env["SHONAR_LLM_API_KEY"] = lanKey
// Rescue summarizer: the LAN GPU intermittently replies
// with garbage; when a summarize job exhausts its retries
// the engine finishes it on local Ollama instead of
// failing, and the detail screen names the swap.
if (ollamaUp()) {
env["SHONAR_LLM_FALLBACK_PROVIDER"] = "ollama"
env["SHONAR_LLM_FALLBACK_BASE_URL"] = "http://127.0.0.1:11434"
env["SHONAR_LLM_FALLBACK_MODEL"] = ollamaModel()
}
} else if (ollamaUp()) {
env["SHONAR_LLM_PROVIDER"] = "ollama"
env["SHONAR_LLM_BASE_URL"] = "http://127.0.0.1:11434"

View file

@ -796,6 +796,35 @@ fun DetailScreen(state: DesktopState) {
)
}
}
// Rescue-swap note: the engine succeeded but had to switch
// summarizers mid-job. Tell the user in plain words, not the raw
// provider ids.
sJob?.takeIf { it.status == "succeeded" && it.error != null }?.let {
val note = it.error!!
.replace("openai_compat", "LAN server (H200)")
.replace("ollama", "this laptop (Ollama)")
Text(note, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.tertiary)
Text("You can change the summarizer in Settings → Summarizer.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant)
}
// Summarize failed even after any rescue: say what broke and how to
// fix it from inside the app.
sJob?.takeIf { it.status == "failed" }?.let {
Text("Summary failed: " + (it.error ?: "unknown error")
.replace("openai_compat", "LAN server (H200)")
.replace("ollama", "this laptop (Ollama)"),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error)
Text("Fix: open Settings → Summarizer and pick \"This laptop " +
"(Ollama)\", then press Retry summary below.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant)
if (detail.busy == null) {
Button({ state.summarize() }) { Text("Retry summary") }
}
}
detail.error?.let {
Text(it, color = MaterialTheme.colorScheme.error)
if (tJob?.status == "failed") {
@ -1167,11 +1196,14 @@ fun SettingsScreen(state: DesktopState) {
}
Text(when (summarizer) {
"lan" -> "Best quality; shares the H200 with other clients — " +
"summaries may wait in its queue before starting."
"summaries may wait in its queue before starting. If it " +
"keeps failing, the job finishes on this laptop's Ollama " +
"and this screen says so."
"local" -> "Private and never queued behind other users; " +
"smaller model, so summaries are plainer."
else -> "Uses the LAN server when its key is available, " +
"otherwise this laptop's Ollama."
else -> "Uses the LAN server when its key is available " +
"(falling back to this laptop's Ollama on repeated " +
"failures), otherwise Ollama straight away."
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant)