Synced from monorepo

Changes:
- Non-blocking coding-data sharing upsell banner
- Consolidate remediation in Doctor
- Auto mode defers fail-closed gate asks to the classifier
- Coalesce marketplace list fetches
- Allow removing a marketplace source by name
- Contain hung git marketplace sources (timeouts, non-blocking refresh, unbrick modal)
- Label failed workspace RPCs with error_kind
- Drop redundant explicit tonic/prost deps from xai-grok-shell
- Report real exit codes for completed background shells
- Narrow the date-rollover reminder to date-bearing templates
- Wire toolOverrides through the session and agent
- Security: Bash(git:*) allowlist matches whole command chain by prefix
- Split prompt-trigger telemetry and record classifier provenance
- Raise connectors-manager timeout to 60s
- Auto classifier honors recorded approvals for repeat actions
- Apply doctor fixes in the TUI
- Auto-mode classifier timeouts prompt instead of silently denying
- Scope subagent completion drains to the owning session
- Add the toolOverrides wire types
- Set client_identifier=grok-agent-sdk
- Accept both spellings of the workspace-teleport kill switch
- Persist one-shot occurrence journal
- Stop turns that poll the exact same tool call 16x in a row
- Copy compaction checkpoint files when forking sessions
- Auto-focus permission prompt from scrollback
- Esc cancels the running turn in non-vim and minimal modes
- List Ctrl+Z undo and redo in keyboard shortcuts
- Out-of-process macOS mic capture
- Show active auth mode on session-info
- Install the npm binary under $GROK_HOME
- Remove hover/click dead zones between dashboard items
- Route startup warnings to doctor
- Document [feedback.user] author identity config
- Extend bang command timeout
- Close combine-queued edit-hold race
- Integrate relocation recovery
- Expose privacy notice rollout flag
- Break harness discovery ref cycle so connections can idle-evict
- Shift/Alt+Enter inserts newline when editing a queued prompt
- Gate project Claude permissions on folder trust
- Echo response.create.event_id on response.created
- Toast when session creation fails from disk full
- Add shared test process lifecycle
- Enable dynamic workflows by default
- Add relocation transaction state machine
- Add shared test sandbox
- Surface auth failures on model-switch compact
- Persist durable scheduler expiry
- Confirm before removing extensions-modal items
- Re-run compact and prompt after login when compact hit expired auth
- Recap sends hosted tools under backend search
This commit is contained in:
grokkybara[bot] 2026-07-22 19:18:53 +01:00
commit a5727c5960
482 changed files with 37627 additions and 13402 deletions

View file

@ -609,6 +609,7 @@ pub enum CancellationCategory {
PermissionRejected,
PermissionCancelled,
MidTurnAbort,
ActionStationarity,
}
// Note: `From<&permission::Decision> for PermissionDecision` crosses the
@ -627,6 +628,7 @@ mod tests {
CancellationCategory::PermissionRejected,
CancellationCategory::PermissionCancelled,
CancellationCategory::MidTurnAbort,
CancellationCategory::ActionStationarity,
] {
let value = serde_json::to_value(variant).unwrap();
let decoded: CancellationCategory = serde_json::from_value(value).unwrap();
@ -648,6 +650,10 @@ mod tests {
"\"permission_cancelled\"",
),
(CancellationCategory::MidTurnAbort, "\"mid_turn_abort\""),
(
CancellationCategory::ActionStationarity,
"\"action_stationarity\"",
),
] {
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(json, expected, "{variant:?} must serialize to {expected}");

View file

@ -422,7 +422,8 @@ pub fn try_remove_temp(path: &Path, stats: Option<&UploadQueueStats>) {
&& e.kind() != std::io::ErrorKind::NotFound
{
tracing::warn!(
path = % path.display(), error = % e,
path = %path.display(),
error = %e,
"Failed to remove upload-queue temp file; leaked"
);
if let Some(s) = stats {
@ -577,7 +578,7 @@ impl UploadQueue {
) -> Self {
let queue_dir = grok_home.join("upload_queue");
if let Err(e) = std::fs::create_dir_all(&queue_dir) {
tracing::warn!(error = % e, "Failed to create upload queue dir");
tracing::warn!(error = %e, "Failed to create upload queue dir");
}
if let Some(raw_secs) = std::env::var("GROK_UPLOAD_QUEUE_AUTH_PROBE_SECS")
.ok()
@ -1327,7 +1328,8 @@ impl UploadQueue {
}
let slice = deadline.min(now + Duration::from_millis(250));
tokio::select! {
_ = notified => {} _ = tokio::time::sleep_until(slice) => {}
_ = notified => {}
_ = tokio::time::sleep_until(slice) => {}
}
}
}
@ -1372,9 +1374,7 @@ impl UploadQueue {
let remaining = self.stats.pending.load(Ordering::Relaxed) as usize;
current_span.record("outcome", "panicked");
current_span.record("remaining", remaining);
tracing::warn!(
error = % e, "Upload queue worker panicked during drain"
);
tracing::warn!(error = %e, "Upload queue worker panicked during drain");
remaining
}
Err(_) => {
@ -1480,31 +1480,35 @@ impl UploadQueue {
.acquire_many_owned(permits)
.await
.map_err(|e| {
tracing::warn!(
error = % e,
"inline-fallback semaphore closed; proceeding ungated"
)
tracing::warn!(error = %e, "inline-fallback semaphore closed; proceeding ungated")
})
.ok();
let wrapped = ResolvedStorageConfig::from_resolver_async(&resolver).await;
let result =
match upload_file(&wrapped, &gcs_path, &source_path, &content_type).await {
Ok(url) => Ok(UploadCompletion {
let wrapped = ResolvedStorageConfig::from_resolver_async(&resolver)
.await;
let result = match upload_file(
&wrapped,
&gcs_path,
&source_path,
&content_type,
)
.await
{
Ok(url) => {
Ok(UploadCompletion {
gcs_url: url,
compression: BlobCompression::None,
original_size,
stored_size: original_size,
}),
Err(e) => {
tracing::warn!(
gcs_path, error = % e, "Inline blocking upload failed"
);
Err(e)
}
};
})
}
Err(e) => {
tracing::warn!(gcs_path, error = %e, "Inline blocking upload failed");
Err(e)
}
};
let _ = completion_tx.send(result);
}
.instrument(parent_span),
.instrument(parent_span),
);
}
/// Inline fallback for `enqueue_file_reference` when the channel is full /
@ -1532,26 +1536,29 @@ impl UploadQueue {
.acquire_many_owned(permits)
.await
.map_err(|e| {
tracing::warn!(
error = % e,
"inline-fallback semaphore closed; proceeding ungated"
)
tracing::warn!(error = %e, "inline-fallback semaphore closed; proceeding ungated")
})
.ok();
let wrapped = ResolvedStorageConfig::from_resolver_async(&resolver).await;
let result = match upload_file(&wrapped, &gcs_path, &snapshot, &content_type).await
let wrapped = ResolvedStorageConfig::from_resolver_async(&resolver)
.await;
let result = match upload_file(
&wrapped,
&gcs_path,
&snapshot,
&content_type,
)
.await
{
Ok(url) => Ok(UploadCompletion {
gcs_url: url,
compression: BlobCompression::None,
original_size,
stored_size: original_size,
}),
Ok(url) => {
Ok(UploadCompletion {
gcs_url: url,
compression: BlobCompression::None,
original_size,
stored_size: original_size,
})
}
Err(e) => {
tracing::warn!(
gcs_path, error = % e,
"Inline snapshot fallback upload failed"
);
tracing::warn!(gcs_path, error = %e, "Inline snapshot fallback upload failed");
Err(e)
}
};
@ -1560,7 +1567,7 @@ impl UploadQueue {
let _ = tx.send(result);
}
}
.instrument(parent_span),
.instrument(parent_span),
);
}
/// Fire-and-forget inline fallback for `enqueue_file` (over-budget /
@ -1585,21 +1592,23 @@ impl UploadQueue {
.acquire_many_owned(permits)
.await
.map_err(|e| {
tracing::warn!(
error = % e,
"inline-fallback semaphore closed; proceeding ungated"
)
tracing::warn!(error = %e, "inline-fallback semaphore closed; proceeding ungated")
})
.ok();
let wrapped = ResolvedStorageConfig::from_resolver_async(&resolver).await;
if let Err(e) = upload_file(&wrapped, &gcs_path, &source_path, &content_type).await
let wrapped = ResolvedStorageConfig::from_resolver_async(&resolver)
.await;
if let Err(e) = upload_file(
&wrapped,
&gcs_path,
&source_path,
&content_type,
)
.await
{
tracing::warn!(
gcs_path, error = % e, "Inline fallback upload failed"
);
tracing::warn!(gcs_path, error = %e, "Inline fallback upload failed");
}
}
.instrument(parent_span),
.instrument(parent_span),
);
}
/// Fire-and-forget inline fallback for the bytes-based `enqueue`
@ -1622,20 +1631,23 @@ impl UploadQueue {
.acquire_many_owned(permits)
.await
.map_err(|e| {
tracing::warn!(
error = % e,
"inline-fallback semaphore closed; proceeding ungated"
)
tracing::warn!(error = %e, "inline-fallback semaphore closed; proceeding ungated")
})
.ok();
let wrapped = ResolvedStorageConfig::from_resolver_async(&resolver).await;
if let Err(e) = upload_bytes(&wrapped, &gcs_path, &content, &content_type).await {
tracing::warn!(
gcs_path, error = % e, "Inline fallback upload failed"
);
let wrapped = ResolvedStorageConfig::from_resolver_async(&resolver)
.await;
if let Err(e) = upload_bytes(
&wrapped,
&gcs_path,
&content,
&content_type,
)
.await
{
tracing::warn!(gcs_path, error = %e, "Inline fallback upload failed");
}
}
.instrument(parent_span),
.instrument(parent_span),
);
}
}
@ -1694,9 +1706,11 @@ async fn dispatch_item(
let consecutive_failures = consecutive_failures.clone();
let draining = draining.clone();
let span = tracing::info_span!(
parent : item.parent_span.clone(), "gcs_queue_upload", artifact = % item
.artifact_name, gcs_path = % item.gcs_path, client_version = % item
.client_version.as_deref().unwrap_or("unknown"),
parent: item.parent_span.clone(),
"gcs_queue_upload",
artifact = %item.artifact_name,
gcs_path = %item.gcs_path,
client_version = %item.client_version.as_deref().unwrap_or("unknown"),
);
tasks.spawn(
async move {
@ -1725,8 +1739,11 @@ async fn circuit_breaker_cooldown(
stats.circuit_breaker_active.store(true, Ordering::Relaxed);
stats.notify_transition();
let interrupted = tokio::select! {
_ = tokio::time::sleep(CIRCUIT_BREAKER_COOLDOWN) => false, _ = shutdown_rx
.as_mut() => { tracing::debug!("upload_queue.shutdown_signal"); true }
_ = tokio::time::sleep(CIRCUIT_BREAKER_COOLDOWN) => false,
_ = shutdown_rx.as_mut() => {
tracing::debug!("upload_queue.shutdown_signal");
true
}
};
stats.circuit_breaker_active.store(false, Ordering::Relaxed);
stats.notify_transition();
@ -1772,11 +1789,24 @@ async fn upload_worker(
consecutive_failures.store(0, Ordering::Relaxed);
}
tokio::select! {
item = rx.recv() => { match item { Some(item) => { dispatch_item(item, &
semaphore, & resolver, & retry_policy, & stats, & consecutive_failures, &
draining_flag, & mut tasks,). await; while tasks.try_join_next().is_some() {}
} None => break false, } } _ = & mut shutdown_rx => {
tracing::debug!("upload_queue.shutdown_signal"); break true; }
item = rx.recv() => {
match item {
Some(item) => {
dispatch_item(
item, &semaphore, &resolver, &retry_policy,
&stats, &consecutive_failures, &draining_flag, &mut tasks,
).await;
// Reap finished tasks so the JoinSet doesn't grow
// unbounded over the worker's lifetime.
while tasks.try_join_next().is_some() {}
}
None => break false,
}
}
_ = &mut shutdown_rx => {
tracing::debug!("upload_queue.shutdown_signal");
break true;
}
}
};
draining_flag.store(true, Ordering::Relaxed);
@ -1940,8 +1970,10 @@ async fn process_item(
consecutive_failures.fetch_add(1, Ordering::Relaxed);
}
tracing::warn!(
attempts = item.attempts, size_bytes = size, outcome = if terminal {
"dropped" } else { "exhausted" }, error = ? e,
attempts = item.attempts,
size_bytes = size,
outcome = if terminal { "dropped" } else { "exhausted" },
error = ?e,
"Upload queue item failed permanently"
);
remove_item_files(&item, Some(stats));
@ -2034,7 +2066,8 @@ async fn upload_with_retries(
Err(e) => match upload_disposition(&e) {
Disposition::Terminal => {
tracing::warn!(
attempt = item.attempts, error = ? e,
attempt = item.attempts,
error = ?e,
"Storage upload failed with a terminal client error (400/403/404); dropping artifact"
);
return Err(e);
@ -2042,7 +2075,8 @@ async fn upload_with_retries(
Disposition::AuthRefresh => {
if !auth_retried {
tracing::info!(
attempt = item.attempts, error = ? e,
attempt = item.attempts,
error = ?e,
"Auth error, re-resolving credentials for one retry"
);
auth_retried = true;
@ -2078,7 +2112,9 @@ async fn upload_with_retries(
AUTH_PARK_WAIT_INTERVAL,
) else {
tracing::warn!(
attempt = item.attempts, parked, error = ? e,
attempt = item.attempts,
parked,
error = ?e,
"Auth error persists after credential refresh, aborting"
);
return Err(e);
@ -2087,7 +2123,8 @@ async fn upload_with_retries(
parked = true;
stats.auth_parked.fetch_add(1, Ordering::Relaxed);
tracing::warn!(
attempt = item.attempts, gcs_path = % item.gcs_path,
attempt = item.attempts,
gcs_path = %item.gcs_path,
"401 persists after credential refresh; parking item until auth recovers"
);
notify_completion(
@ -2122,8 +2159,10 @@ async fn upload_with_retries(
}
let delay = policy.backoff_delay(item.attempts - 1);
tracing::debug!(
attempt = item.attempts, delay_ms = delay.as_millis() as u64,
error = ? e, "Upload queue item failed, retrying"
attempt = item.attempts,
delay_ms = delay.as_millis() as u64,
error = ?e,
"Upload queue item failed, retrying"
);
tokio::time::sleep(delay).await;
}
@ -2264,7 +2303,8 @@ fn move_or_copy_to_queue_with(
Ok(()) => return Ok(()),
Err(e) => {
tracing::warn!(
source = % source.display(), error = % e,
source = %source.display(),
error = %e,
"rename within queue_dir failed; falling back to copy + remove"
);
copy_fn(source, dest)?;
@ -2349,7 +2389,9 @@ fn cleanup_queue_dir(queue_dir: &Path, max_age: Duration, stats: Option<&UploadQ
}
if cleaned > 0 {
tracing::info!(
cleaned, cleaned_bytes, dir = % queue_dir.display(),
cleaned,
cleaned_bytes,
dir = %queue_dir.display(),
"Cleaned up orphaned upload queue entries from previous session"
);
}
@ -4248,8 +4290,8 @@ mod tests {
return true;
}
tokio::select! {
r = rx.changed() => r.is_ok(), _ = tokio::time::sleep(slice) =>
false,
r = rx.changed() => r.is_ok(),
_ = tokio::time::sleep(slice) => false,
}
}))
}

View file

@ -546,7 +546,7 @@ impl StorageClient {
/// These become the headers:
/// - `x-grok-client-version`
/// - `x-grok-client-identifier` (one of "grok-shell", "grok-pager",
/// "grok-desktop", "grok-extension")
/// "grok-desktop", "grok-extension", "grok-agent-sdk")
///
/// Server-side logs in `cli-chat-proxy` and analytics queries now
/// surface these values, making it easy to attribute 400/403 errors to