Offer Claude sign-in from failed sessions

This commit is contained in:
iris-ai committed 2026-09-15 12:24:25 -04:00
1 parent 0be15adbee
commit f0661919bb
9 files changed
+140 -12

No files matched your search

+39 -3
View File
@@ -367,10 +367,20 @@ impl Translator {
.unwrap_or(false)
{
let said = message.get("result").and_then(Value::as_str);
events.push(match said.and_then(usage_limit) {
Some(resets_at) => Event::LimitReached { resets_at },
events.push(match said {
Some(message) if authentication_required(message) => {
Event::AuthenticationRequired {
message: message.to_string(),
}
}
Some(message) => match usage_limit(message) {
Some(resets_at) => Event::LimitReached { resets_at },
None => Event::Error {
message: message.to_string(),
},
},
None => Event::Error {
message: said.unwrap_or("the turn ended with an error").to_string(),
message: "the turn ended with an error".to_string(),
},
});
}
@@ -1171,6 +1181,13 @@ fn usage_limit(result: &str) -> Option<Option<f64>> {
Some(stamp)
}
/// Claude Code's actionable login failure, kept here with its other dialect strings.
fn authentication_required(message: &str) -> bool {
message
.to_ascii_lowercase()
.contains("oauth session expired and could not be refreshed")
}
/// A string field that is there and not empty, or `None`. The CLI omits these
/// rather than sending them empty, but a caller that sends `""` means the same
/// thing and should not produce a description that draws as a blank line.
@@ -2482,6 +2499,25 @@ mod tests {
);
}
#[test]
fn an_expired_login_is_actionable_above_the_driver() {
let dir = tempfile::tempdir().expect("tempdir");
let mut translator = Translator::new(dir.path().to_path_buf(), test_subagents(&dir));
let events = translate_lines(
&mut translator,
&[
r#"{"type":"result","subtype":"error_during_execution","is_error":true,"result":"Failed to authenticate: OAuth session expired and could not be refreshed","usage":{}}"#,
],
);
assert_eq!(
events[0],
Event::AuthenticationRequired {
message: "Failed to authenticate: OAuth session expired and could not be refreshed"
.to_string()
}
);
}
/// Running out of quota is a state, not a failure of the work.
///
/// The naive reading -- an error result like any other -- is what shipped
+8
View File
@@ -440,6 +440,14 @@ pub enum Event {
#[serde(default, skip_serializing_if = "Option::is_none")]
resets_at: Option<f64>,
},
/// The provider refused the turn because its login can no longer be used.
///
/// Recognised by the driver for the same reason [`Event::LimitReached`] is:
/// only that layer knows the provider's dialect, and the phone needs a
/// state it can act on without matching error prose.
AuthenticationRequired {
message: String,
},
Error {
message: String,
},
+3
View File
@@ -873,6 +873,9 @@ mod tests {
tokens: 42,
context: Some(42),
},
Event::AuthenticationRequired {
message: "sign in again".into(),
},
Event::Error {
message: "boom".into(),
},