Synced from monorepo
Synced from monorepo Changes: - Release a shell session's resources in one drop - Make the tools blocking-wait cap client-configurable and self-describing - Recognize API "exceeds budget" errors as context overflow - Retry /btw on model overload - Carry running background tasks and subagents across compaction - Require round-trip time for SDK liveness checks - Background-subagent completion reminders with a selectable delivery surface - Make a PTY shell reap itself until it reaches the registry - Recover the OS error code from a TLS-phase connection reset - Consume the attached-client signal and report why idle is withheld - Treat `.grok/sandbox.toml` edits as protected so auto mode prompts before writing - Surface history/search in the Ctrl+. cheatsheet and keep it working in history view - Delete sessions from the dashboard and welcome list - Release a session's activity record when the session ends - Stop charging auth-retry budget for fail-closed 401s; reset it across suspends - Scope skills watches on project vendor roots - Make [stop] cancel in-flight compaction - Make the leader soak measure the leader, not its harness Source-Revision: 8d69c91f02bcacf01e98d5aebbf2f92547c45738
This commit is contained in:
parent
dd04f397b1
commit
a422116582
165 changed files with 15161 additions and 1969 deletions
|
|
@ -415,6 +415,40 @@ pub fn error_cause_chain(err: &dyn std::error::Error) -> String {
|
|||
msg
|
||||
}
|
||||
|
||||
/// First OS error code in `err`'s `source()` chain (e.g. 104 `ECONNRESET` on
|
||||
/// Linux, 10054 on Windows), preferring [`std::io::Error::raw_os_error`] and
|
||||
/// falling back to the `(os error N)` suffix `io::Error`'s `Display` appends.
|
||||
///
|
||||
/// The fallback is load-bearing: a reset during the TLS handshake arrives as a
|
||||
/// *custom* `io::Error` (kind `Other`, no raw code) whose only record of the
|
||||
/// code is that suffix, and without it a rustls reset is indistinguishable
|
||||
/// from an unreachable host.
|
||||
///
|
||||
/// `+ 'static` because `downcast_ref` resolves the type through
|
||||
/// [`std::any::Any`], whose type ids only exist for `'static` types.
|
||||
pub fn find_os_error_code(err: &(dyn std::error::Error + 'static)) -> Option<i32> {
|
||||
let mut cur: Option<&(dyn std::error::Error + 'static)> = Some(err);
|
||||
while let Some(e) = cur {
|
||||
if let Some(code) = e.downcast_ref::<std::io::Error>().and_then(|ioe| {
|
||||
ioe.raw_os_error()
|
||||
.or_else(|| parse_os_error(&ioe.to_string()))
|
||||
}) {
|
||||
return Some(code);
|
||||
}
|
||||
cur = e.source();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract `N` from a message ending in `(os error N)`.
|
||||
fn parse_os_error(msg: &str) -> Option<i32> {
|
||||
msg.rsplit_once("(os error ")?
|
||||
.1
|
||||
.trim_end_matches(')')
|
||||
.parse()
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// How a `reqwest` request/send failure should be treated by a retry loop.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum TransportFailureKind {
|
||||
|
|
@ -601,6 +635,104 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_os_error_code_walks_source_chain() {
|
||||
#[derive(Debug)]
|
||||
struct IoLeaf(std::io::Error);
|
||||
impl std::fmt::Display for IoLeaf {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "io leaf")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for IoLeaf {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
Some(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Wrapper(IoLeaf);
|
||||
impl std::fmt::Display for Wrapper {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "wrapper")
|
||||
}
|
||||
}
|
||||
impl std::error::Error for Wrapper {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
Some(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
let err = Wrapper(IoLeaf(std::io::Error::from_raw_os_error(104)));
|
||||
assert_eq!(find_os_error_code(&err), Some(104));
|
||||
assert_eq!(find_os_error_code(&std::io::Error::other("no code")), None);
|
||||
}
|
||||
|
||||
/// A TLS-handshake reset arrives as a custom `io::Error` with no raw code;
|
||||
/// live reqwest gives the chain `client error (Connect)` →
|
||||
/// `Connection reset by peer (os error 54)`.
|
||||
#[test]
|
||||
fn recovers_code_from_a_custom_io_error() {
|
||||
let tls_shaped = std::io::Error::other("Connection reset by peer (os error 54)");
|
||||
assert_eq!(tls_shaped.raw_os_error(), None, "precondition: no raw code");
|
||||
assert_eq!(find_os_error_code(&tls_shaped), Some(54));
|
||||
|
||||
let windows_shaped = std::io::Error::other(
|
||||
"An existing connection was forcibly closed by the remote host. (os error 10054)",
|
||||
);
|
||||
assert_eq!(find_os_error_code(&windows_shaped), Some(10054));
|
||||
}
|
||||
|
||||
/// Over a real socket: a mid-request reset must classify as `Interrupted`
|
||||
/// *and* surface the OS code, which is what lets a fleet report tell "peer
|
||||
/// reset us" from "server unreachable".
|
||||
///
|
||||
/// Lives here rather than in a caller's crate: a `reqwest` client drags
|
||||
/// rustls into the test binary, which not every caller's tests tolerate.
|
||||
#[test]
|
||||
fn real_connection_reset_classifies_as_interrupted_with_os_code() {
|
||||
// Closing a socket whose receive queue still holds the request emits
|
||||
// RST instead of FIN.
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
|
||||
let port = listener.local_addr().expect("addr").port();
|
||||
std::thread::spawn(move || {
|
||||
let sock = listener.accept().expect("accept").0;
|
||||
sock.set_read_timeout(Some(std::time::Duration::from_secs(5)))
|
||||
.expect("read timeout");
|
||||
let _ = sock.peek(&mut [0u8; 64]);
|
||||
drop(sock);
|
||||
});
|
||||
|
||||
let err = reqwest::blocking::Client::new()
|
||||
.get(format!("http://127.0.0.1:{port}/oauth2/device/code"))
|
||||
.send()
|
||||
.expect_err("reset must fail the request");
|
||||
|
||||
assert_eq!(
|
||||
TransportFailure::classify(&err).kind,
|
||||
TransportFailureKind::Interrupted
|
||||
);
|
||||
assert!(
|
||||
// ECONNRESET: 54 on macOS, 104 on Linux, 10054 on Windows.
|
||||
matches!(find_os_error_code(&err), Some(54 | 104 | 10054)),
|
||||
"reset must carry an OS code, got {:?}",
|
||||
find_os_error_code(&err)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_os_error_ignores_messages_without_a_code() {
|
||||
assert_eq!(
|
||||
parse_os_error("connection closed before message completed"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
parse_os_error("invalid peer certificate (os error oops)"),
|
||||
None
|
||||
);
|
||||
assert_eq!(parse_os_error("broken pipe (os error 32)"), Some(32));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn origin_client_info_from_meta_extracts_identifier_and_version() {
|
||||
let meta = serde_json::json!({
|
||||
|
|
|
|||
Loading…
Reference in a new issue