Synced from monorepo

Synced from monorepo

Changes:
- grok-shell: send an expired external-provider credential to the sign-in flow, not a 401 loop
- pager: clickable ▲ jumps to the top of the response being read
- grok-shell: keep a large task log from making the completion message too long
- Plan viewer scrollbar: widen grab zone to the border column; fix striped thumb in Terminal.app
- pager: poll the tmux probe teardown grace instead of sleeping it
- security: vendor-compat MCP kill switch is now actually enforced when reported as on
- grok-shell: restore session eviction when a leader client disconnects
- Bump rust-toolchain to 1.93.0
- workspace: lexical-normalize permission path patterns before glob matching
- pager: reject garbage Enter in the /resume picker
- pager: show Mermaid affordances in plan mode preview
- pager: drop manage-account link from /session-info
- workspace: auto-approve read-only git queries; defer write floor to auto classifier
- Add free-form pattern editor to the "Always allow" command prompt
- grok-shell: fix /btw caching
- pager: Tab walks answers in the ask_user_question card
- External-provider auth refresh: single 7s attempt instead of 3×5s
- pager: don't resurrect finished background tasks as Running when completion arrives first
- pager: report tmux truecolor clamping in Doctor
- Fix plan viewer scrollbar click+drag hijacked by comment gutter
- pager/shell: stop double Recap after the same last turn
- sampler: preserve x-should-retry through stream collection
- pager: clear plan-mode indicator immediately when the user approves a plan
- pager: tmux does not re-read its config on reattach

Source-Revision: 64c4de99cc822b25ce9c54ab5a4f372093d0885d
This commit is contained in:
grokkybara[bot] 2026-08-03 08:17:57 +00:00
commit 780d1388ff
323 changed files with 12258 additions and 7226 deletions

View file

@ -714,7 +714,7 @@ fn synthesize_from_info(info: &SamplingErrorInfo) -> SamplingError {
message: info.message.clone(),
model_metadata: info.model_metadata.clone(),
retry_after_secs: info.retry_after_secs,
should_retry: None,
should_retry: info.should_retry,
}
}
SamplingErrorKind::EmptyResponse => {
@ -825,6 +825,7 @@ fn handle_cancellation(
message: "request cancelled".to_string(),
is_retryable: false,
retry_after_secs: None,
should_retry: None,
model_metadata: None,
empty_response_context: None,
doom_loop_triggers: None,
@ -863,6 +864,7 @@ mod tests {
message: "inference idle timeout after 240s with no chunks".to_string(),
is_retryable: false,
retry_after_secs: None,
should_retry: None,
model_metadata: None,
empty_response_context: None,
doom_loop_triggers: None,
@ -884,6 +886,7 @@ mod tests {
message: "boom".to_string(),
is_retryable: true,
retry_after_secs: None,
should_retry: Some(false),
model_metadata: None,
empty_response_context: None,
doom_loop_triggers: None,
@ -893,10 +896,14 @@ mod tests {
let err = synthesize_from_info(&info);
match err {
SamplingError::Api {
status, message, ..
status,
message,
should_retry,
..
} => {
assert_eq!(status.as_u16(), 500);
assert_eq!(message, "boom");
assert_eq!(should_retry, Some(false), "server veto must survive");
}
other => panic!("expected Api, got {other:?}"),
}
@ -910,6 +917,7 @@ mod tests {
message: "slow down".to_string(),
is_retryable: true,
retry_after_secs: Some(7),
should_retry: None,
model_metadata: None,
empty_response_context: None,
doom_loop_triggers: None,

View file

@ -31,6 +31,7 @@ use xai_grok_sampling_types::{
use crate::attribution::bearer_tail_fragment;
use crate::config::{AuthScheme, OriginClientInfo, SamplerConfig};
use crate::events::SamplingErrorInfo;
// Re-export ApiBackend from the shared types crate for downstream callers.
pub use xai_grok_sampling_types::ApiBackend;
@ -2056,16 +2057,22 @@ impl SamplingClient {
};
result
.map(|(response, _metrics)| response)
.map_err(|info| SamplingError::Api {
status: info
.status_code
.and_then(|c| reqwest::StatusCode::from_u16(c).ok())
.unwrap_or(reqwest::StatusCode::INTERNAL_SERVER_ERROR),
message: info.message,
model_metadata: info.model_metadata,
retry_after_secs: info.retry_after_secs,
should_retry: None,
})
.map_err(stream_collect_error)
}
}
/// Rebuild `Api` from stream-collected info, preserving status,
/// `Retry-After`, and `x-should-retry` (kind is lost on this path).
fn stream_collect_error(info: SamplingErrorInfo) -> SamplingError {
SamplingError::Api {
status: info
.status_code
.and_then(|c| reqwest::StatusCode::from_u16(c).ok())
.unwrap_or(reqwest::StatusCode::INTERNAL_SERVER_ERROR),
message: info.message,
model_metadata: info.model_metadata,
retry_after_secs: info.retry_after_secs,
should_retry: info.should_retry,
}
}
@ -2075,6 +2082,45 @@ mod tests {
use indexmap::IndexMap;
use xai_grok_sampling_types::types::ChatRequestMessage;
#[test]
fn stream_collect_error_preserves_should_retry() {
let info = SamplingErrorInfo {
kind: crate::events::SamplingErrorKind::Api,
status_code: Some(529),
message: "Overloaded".into(),
is_retryable: true,
retry_after_secs: Some(3),
should_retry: Some(false),
model_metadata: None,
empty_response_context: None,
doom_loop_triggers: None,
doom_loop_aborted_at_chunk: None,
credential: xai_grok_sampling_types::SentCredential::Unknown,
};
// SamplingError is not PartialEq (it carries reqwest/serde errors),
// so destructure once and compare all fields in a single assert.
let SamplingError::Api {
status,
message,
model_metadata,
retry_after_secs,
should_retry,
} = stream_collect_error(info)
else {
panic!("expected Api");
};
assert_eq!(
(
status.as_u16(),
message.as_str(),
model_metadata.is_none(),
retry_after_secs,
should_retry,
),
(529, "Overloaded", true, Some(3), Some(false)),
);
}
fn minimal_config() -> SamplerConfig {
SamplerConfig {
api_key: Some("test-key".to_string()),

View file

@ -154,6 +154,11 @@ pub struct SamplingErrorInfo {
pub message: String,
pub is_retryable: bool,
pub retry_after_secs: Option<u64>,
/// Parsed `x-should-retry` response header. `Some(false)` = the server
/// says the failure is request-content-caused; never retry. `None` =
/// header absent, or payload from an older peer.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub should_retry: Option<bool>,
pub model_metadata: Option<ResponseModelMetadata>,
/// Present only when `kind == EmptyResponse`. Carries the structured
/// context from the L2 stream so downstream consumers can distinguish
@ -279,6 +284,7 @@ impl From<&SamplingError> for SamplingErrorInfo {
kind,
status_code,
message,
should_retry: err.should_retry_header(),
is_retryable,
retry_after_secs,
model_metadata,
@ -295,6 +301,26 @@ mod tests {
use super::*;
use reqwest::StatusCode;
#[test]
fn from_sampling_error_carries_should_retry_header() {
let err = SamplingError::Api {
status: StatusCode::from_u16(529).expect("valid status"),
message: "Overloaded".into(),
model_metadata: None,
retry_after_secs: None,
should_retry: Some(false),
};
let info = SamplingErrorInfo::from(&err);
assert_eq!(info.should_retry, Some(false));
// Non-Api variants have no header — stays None.
let stream_err = SamplingError::StreamError {
error_type: "overloaded_error".into(),
message: "Overloaded".into(),
};
assert_eq!(SamplingErrorInfo::from(&stream_err).should_retry, None);
}
#[test]
fn auth_variant_classified_as_auth() {
let err = SamplingError::auth_unknown("bad token");

View file

@ -49,6 +49,7 @@ pub async fn collect_response(
message: "stream ended without Completed or Failed".to_string(),
is_retryable: false,
retry_after_secs: None,
should_retry: None,
model_metadata: None,
empty_response_context: None,
doom_loop_triggers: None,