From 33bfc5e1ebe8501ebea09ff0e22043814e534585 Mon Sep 17 00:00:00 2001 From: Edwin Date: Sun, 2 Aug 2026 13:05:08 -0700 Subject: [PATCH] Fall back to the harness probe when smith can't name a session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-naming picks its generator by asking whether smith is available at all — a question OAuth-only and Ollama-only machines answer "yes" while title generation on them can only fail. Those users burned their one naming attempt on a doomed one-shot and kept hash names, even though the same-harness probe fallback (spec 0151) was written for exactly them. Ask the narrower question instead: can title generation resolve a model? That means an explicit pin naming a provider it can build, or one of the direct API keys on its own ladder — deliberately narrower than the session-startability check, whose extra rungs spec 0071 records as intentional. Second, a generator that runs and produces nothing no longer ends the attempt. The per-session latch is one-way and only a daemon restart clears it, so a network blip or a revoked key stranded the session on its hash name for the rest of the run. A failed one-shot now hands off to the probe. Generators distinguish "nothing to do" from "produced nothing": a session deleted or renamed by hand mid-flight counts as settled, so the handoff can never overwrite a title the user chose. The feature-status/doctor row for auto-naming moves to the same narrow predicate. It reports on the generator auto-naming actually uses, and reporting Ok off the wider check promised a generator that never runs. --- crates/daemon/src/availability.rs | 128 +++++++- crates/daemon/src/doctor.rs | 1 + crates/daemon/src/session.rs | 283 +++++++++++++----- ...151-ambient-feature-status-and-fallback.md | 35 ++- 4 files changed, 365 insertions(+), 82 deletions(-) diff --git a/crates/daemon/src/availability.rs b/crates/daemon/src/availability.rs index 8ac89f39..d3cc2a58 100644 --- a/crates/daemon/src/availability.rs +++ b/crates/daemon/src/availability.rs @@ -202,6 +202,51 @@ pub async fn probe_smith(cache: &std::sync::Mutex) -> Availab Availability::missing("no API key or OAuth credential found") } +/// Model-spec prefixes the title-gen one-shot cannot build a provider for. +/// OAuth subscriptions drive full smith sessions fine; `--title-mode` only +/// knows the direct-API-key providers and bails loudly on these (spec +/// 0071). Kept in sync with title-mode's own provider selection. +const TITLE_GEN_UNSUPPORTED_PREFIXES: [&str; 4] = + ["claude-oauth", "codex-oauth", "grok-oauth", "kimi-oauth"]; + +/// Whether the cheap `smith --title-mode` one-shot could actually resolve a +/// model: an explicit `CONSTRUCT_SMITH_MODEL` pin naming a provider it can +/// build, or one of the direct API keys on its ladder. +/// +/// Deliberately narrower than [`probe_smith`], which answers "could a +/// session start via *some* explicit choice" and therefore counts OAuth +/// subscriptions and a reachable Ollama — the asymmetry spec 0071 records +/// as intentional. Auto-title needs the narrower question: asking the wider +/// one sends an OAuth-only machine into a one-shot that can only fail, +/// instead of the same-harness probe fallback written for exactly that +/// machine (spec 0151). +pub fn smith_title_gen_available() -> bool { + title_gen_available_with(crate::daemon_env::var) +} + +/// Pure core of [`smith_title_gen_available`], parameterized over the +/// environment lookup so tests don't have to mutate process env. +fn title_gen_available_with(lookup: impl Fn(&str) -> Option) -> bool { + // A pin is an explicit user choice, so it wins over the key ladder the + // same way it does inside title-mode — but only when title-mode can + // actually honor it. + if let Some(pin) = lookup("CONSTRUCT_SMITH_MODEL") { + let prefix = pin.split(':').next().unwrap_or_default(); + return !TITLE_GEN_UNSUPPORTED_PREFIXES.contains(&prefix); + } + [ + "ANTHROPIC_API_KEY", + "OPENAI_API_KEY", + "GEMINI_API_KEY", + "GOOGLE_API_KEY", + "META_API_KEY", + "MODEL_API_KEY", + "DEEPSEEK_API_KEY", + ] + .iter() + .any(|k| lookup(k).is_some()) +} + /// Existence-only mirror of `CredStore::locate` in /// `adapter-smith/src/provider/claude_oauth.rs`: explicit file override, /// then the default credentials file, then the macOS keychain item. No @@ -503,6 +548,11 @@ pub async fn smith_auth_methods( pub struct FeatureInputs { /// smith's credential probe result ([`probe_smith`]). pub smith: Availability, + /// Whether the title-gen one-shot can resolve a model + /// ([`smith_title_gen_available`]). Narrower than `smith`: an + /// OAuth-only machine can run smith sessions but not `--title-mode`, + /// and the auto-title row must report what auto-title actually does. + pub title_gen: bool, /// `[suggest] enabled` from config. pub suggest_enabled: bool, /// The orchestrator's configured harness and that harness's @@ -517,7 +567,11 @@ pub struct FeatureInputs { pub fn ambient_features(inputs: &FeatureInputs) -> Vec { use construct_protocol::{FeatureInfo, FeatureStatus}; let smith_ok = inputs.smith.available; - let auto_title = if smith_ok { + // Auto-title keys off `title_gen`, not `smith_ok`: a machine with only + // an OAuth subscription runs smith sessions fine but cannot run the + // title one-shot, and reporting Ok there promises a generator that + // never runs. + let auto_title = if inputs.title_gen { FeatureInfo { id: "auto_title".to_string(), label: "Session auto-naming".to_string(), @@ -529,9 +583,9 @@ pub fn ambient_features(inputs: &FeatureInputs) -> Vec