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

@ -107,9 +107,7 @@ impl ChatModesManager {
}
Ok(_) => empty_state(),
Err(err) => {
tracing::warn!(
error = % err, "chat modes fetch failed; serving cache/empty"
);
tracing::warn!(error = %err, "chat modes fetch failed; serving cache/empty");
let guard = self.inner.cache.read();
match guard.as_ref() {
Some(c) if c.user_id == user_id => modes_to_model_state(&c.response),
@ -232,7 +230,7 @@ mod tests {
Mode {
id: id.to_owned(),
availability: ModeAvailability {
requires_upgrade: Some(serde_json::json!({ "message" : "Upgrade" })),
requires_upgrade: Some(serde_json::json!({ "message": "Upgrade" })),
..Default::default()
},
..Default::default()

View file

@ -444,7 +444,8 @@ impl EndpointsConfig {
std::fs::read_to_string(path)
.inspect_err(|e| {
tracing::warn!(
path = % path, error = % e,
path = %path,
error = %e,
"Failed to read trace upload credentials file"
);
})
@ -482,7 +483,7 @@ impl EndpointsConfig {
});
}
tracing::warn!(
bucket = % bucket_url,
bucket = %bucket_url,
"trace_upload_bucket has unrecognized scheme (expected gs:// or s3://), ignoring"
);
None
@ -1099,6 +1100,7 @@ pub struct HarnessConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub upload_flush_timeout_secs: Option<u64>,
}
impl HarnessConfig {}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct RelayConfig {
@ -1994,10 +1996,10 @@ impl Config {
Some("auth"),
super::config_model_override_parse::ConfigWarningKind::ConflictingFields,
format!(
"inline auth overwrites a hand-written \
"inline auth overwrites a hand-written \
[auth_provider.\"{synthetic}\"]; the `model_provider:` prefix is \
a reserved namespace"
),
),
),
);
}
@ -2346,19 +2348,23 @@ impl Config {
let telemetry = self.resolve_telemetry_mode();
let trace_upload = self.resolve_trace_upload();
let req = &self.requirements.trace_upload;
serde_json::json!(
{ "trace_upload" : trace_upload.value, "trace_upload_source" : trace_upload
.source.to_string(), "telemetry_mode" : telemetry.value.to_string(),
"telemetry_source" : telemetry.source.to_string(), "in_requirement_pin" : req
.pinned(), "in_requirement_src" : req.source().map(| s | s.to_string()),
"in_env_trace_upload" : std::env::var("GROK_TELEMETRY_TRACE_UPLOAD").ok(),
"in_env_telemetry_enabled" : std::env::var("GROK_TELEMETRY_ENABLED").ok(),
"in_cfg_telemetry_trace_upload" : self.telemetry.trace_upload,
"in_cfg_features_telemetry" : self.features.telemetry.map(| m | m
.to_string()), "in_remote_trace_upload_enabled" : self.remote_settings
.as_ref().and_then(| s | s.trace_upload_enabled), "has_remote_settings" :
self.remote_settings.is_some(), }
)
serde_json::json!({
"trace_upload": trace_upload.value,
"trace_upload_source": trace_upload.source.to_string(),
"telemetry_mode": telemetry.value.to_string(),
"telemetry_source": telemetry.source.to_string(),
"in_requirement_pin": req.pinned(),
"in_requirement_src": req.source().map(|s| s.to_string()),
"in_env_trace_upload": std::env::var("GROK_TELEMETRY_TRACE_UPLOAD").ok(),
"in_env_telemetry_enabled": std::env::var("GROK_TELEMETRY_ENABLED").ok(),
"in_cfg_telemetry_trace_upload": self.telemetry.trace_upload,
"in_cfg_features_telemetry": self.features.telemetry.map(|m| m.to_string()),
"in_remote_trace_upload_enabled": self
.remote_settings
.as_ref()
.and_then(|s| s.trace_upload_enabled),
"has_remote_settings": self.remote_settings.is_some(),
})
}
pub(crate) fn resolve_feedback(&self) -> Resolved<bool> {
let ff = self
@ -2514,21 +2520,35 @@ impl Config {
.default(true)
.resolve()
}
/// `image_gen` tool gate. Default on; gated only by the `GROK_IMAGE_GEN`
/// env var and managed-config requirement pin.
/// `image_gen` (+ `/imagine`). Default on.
///
/// `imagine_tools_disabled` is a remote force-off (env/config cannot
/// re-enable). Otherwise: requirement > env > `[features]` > remote >
/// default.
pub(crate) fn resolve_image_gen(&self) -> Resolved<bool> {
use xai_grok_tools::implementations::grok_build::IMAGE_GEN_TOOL_NAME;
if let Some(pinned) = self.requirements.image_gen.pinned() {
return Resolved::new(pinned, ConfigSource::Requirement);
}
if self
.remote_settings
.as_ref()
.is_some_and(|s| s.imagine_tool_disabled(IMAGE_GEN_TOOL_NAME))
{
return Resolved::new(false, ConfigSource::Remote);
}
BoolFlag::env("GROK_IMAGE_GEN")
.requirement(self.requirements.image_gen.pinned())
.config(self.features.image_gen)
.feature_flag(
self.remote_settings
.as_ref()
.and_then(|s| s.image_gen_enabled),
)
.default(true)
.resolve()
}
/// `image_edit` tool gate.
///
/// The remote settings `imagine_tools_disabled` denylist is authoritative:
/// when it lists `image_edit`, the tool is force-removed and local
/// env/config can't re-enable it. A managed requirement pin still outranks
/// it; otherwise the tool defaults on and is overridable via
/// `GROK_IMAGE_EDIT`.
/// `image_edit` tool gate. Same denylist / requirement pattern as
/// [`Self::resolve_image_gen`]; no `[features]` key (defaults on).
pub(crate) fn resolve_image_edit(&self) -> Resolved<bool> {
use xai_grok_tools::implementations::grok_build::IMAGE_EDIT_TOOL_NAME;
if let Some(pinned) = self.requirements.image_edit.pinned() {
@ -2543,6 +2563,34 @@ impl Config {
}
BoolFlag::env("GROK_IMAGE_EDIT").default(true).resolve()
}
/// `image_to_video` / `reference_to_video` (+ `/imagine-video`). Default on.
///
/// Registered as a pair; denylisting either tool name (or `video_gen`)
/// disables both. Otherwise same precedence as [`Self::resolve_image_gen`].
pub(crate) fn resolve_video_gen(&self) -> Resolved<bool> {
use xai_grok_tools::implementations::grok_build::{
IMAGE_TO_VIDEO_TOOL_NAME, REFERENCE_TO_VIDEO_TOOL_NAME,
};
if let Some(pinned) = self.requirements.video_gen.pinned() {
return Resolved::new(pinned, ConfigSource::Requirement);
}
if self.remote_settings.as_ref().is_some_and(|s| {
s.imagine_tool_disabled(IMAGE_TO_VIDEO_TOOL_NAME)
|| s.imagine_tool_disabled(REFERENCE_TO_VIDEO_TOOL_NAME)
|| s.imagine_tool_disabled("video_gen")
}) {
return Resolved::new(false, ConfigSource::Remote);
}
BoolFlag::env("GROK_VIDEO_GEN")
.config(self.features.video_gen)
.feature_flag(
self.remote_settings
.as_ref()
.and_then(|s| s.video_gen_enabled),
)
.default(true)
.resolve()
}
/// Optional Imagine model override for `image_gen`. When set (non-empty),
/// `image_gen` calls this model slug instead of the default quality model.
/// Precedence: env `GROK_IMAGE_GEN_MODEL_OVERRIDE` > `[features]
@ -2574,6 +2622,10 @@ impl Config {
.default(true)
.resolve()
}
/// Background workflows (`workflow` tool, `.grok/workflows/*.rhai`,
/// `/deep-research`, host-owned `/goal` driver). Default ON: deployments
/// that never receive remote settings still get workflows; `Some(false)`
/// remote / config / env remains a kill-switch.
pub(crate) fn resolve_workflows(&self) -> Resolved<bool> {
let ff = self
.remote_settings
@ -2585,7 +2637,7 @@ impl Config {
BoolFlag::env("GROK_WORKFLOWS")
.config(self.workflows.enabled)
.feature_flag(ff)
.default(false)
.default(true)
.resolve()
}
/// Classifier, planner, and summary all default to goal mode itself: when
@ -3387,8 +3439,8 @@ pub fn resolve_model_list(
let mut resolved: IndexMap<String, ModelEntry> = IndexMap::new();
if cfg.endpoints.has_custom_endpoint() {
tracing::info!(
models_base_url = ? cfg.endpoints.models_base_url, models_list_url = ? cfg
.endpoints.models_list_url,
models_base_url = ?cfg.endpoints.models_base_url,
models_list_url = ?cfg.endpoints.models_list_url,
"custom models endpoint active, skipping built-in defaults",
);
} else {
@ -3406,9 +3458,11 @@ pub fn resolve_model_list(
&& donor.info.context_window.get() != default_cw
{
tracing::debug!(
model_key = % key, model = % entry.info.model, client_default =
default_cw, inherited = donor.info.context_window.get(),
donor_model = % donor.info.model,
model_key = %key,
model = %entry.info.model,
client_default = default_cw,
inherited = donor.info.context_window.get(),
donor_model = %donor.info.model,
"prefetched model missing context_window, inheriting from hardcoded default"
);
entry.info.context_window = donor.info.context_window;
@ -3421,9 +3475,7 @@ pub fn resolve_model_list(
}
}
if resolved.contains_key(key) {
tracing::debug!(
model_key = % key, "prefetched model overriding default"
);
tracing::debug!(model_key = %key, "prefetched model overriding default");
}
}
resolved = prefetched;
@ -3432,13 +3484,11 @@ pub fn resolve_model_list(
let had_base = resolved.contains_key(key);
let base = resolved.shift_remove(key);
if !had_base {
tracing::debug!(
model_key = % key,
"config model adding new entry (not in defaults/prefetched)"
);
tracing::debug!(model_key = %key, "config model adding new entry (not in defaults/prefetched)");
if model_override.context_window.is_none() {
tracing::debug!(
model_key = % key, default = 200_000,
model_key = %key,
default = 200_000,
"new model missing context_window, defaulting to 200000 — set context_window in [model.{}] to override",
key,
);
@ -3466,10 +3516,13 @@ pub fn resolve_model_list(
)));
}
tracing::debug!(
model_key = % key, base_url = % entry.info.base_url, has_api_key = entry
.api_key.is_some(), env_key = ? entry.env_key, auth_provider = entry
.auth_provider.as_ref().map(| p | p.name.as_str()), model_provider =
model_override.model_provider.as_deref(), had_base,
model_key = %key,
base_url = %entry.info.base_url,
has_api_key = entry.api_key.is_some(),
env_key = ?entry.env_key,
auth_provider = entry.auth_provider.as_ref().map(|p| p.name.as_str()),
model_provider = model_override.model_provider.as_deref(),
had_base,
"config model override applied"
);
resolved.insert(key.clone(), entry);
@ -3482,7 +3535,8 @@ pub fn resolve_model_list(
let config = cfg.auth_providers.get(&provider.name);
if config.is_none() {
tracing::debug!(
model_key = % key, provider = % provider.name,
model_key = %key,
provider = %provider.name,
"provider ref has no trusted config; failing closed with an empty command"
);
}
@ -3506,8 +3560,9 @@ pub fn resolve_model_list(
if let Some((donor_cw, donor_backend)) = donors.get(&entry.info.model) {
if entry.info.context_window.get() == default_cw {
tracing::debug!(
model = % entry.info.model, from = default_cw, to = donor_cw
.get(),
model = %entry.info.model,
from = default_cw,
to = donor_cw.get(),
"slug-match: inheriting context_window from sibling catalog entry"
);
entry.info.context_window = *donor_cw;
@ -3522,7 +3577,7 @@ pub fn resolve_model_list(
}
if let Some(ref global_agent_type) = cfg.models.agent_type {
tracing::warn!(
global_agent_type = % global_agent_type,
global_agent_type = %global_agent_type,
"[models] agent_type is deprecated. Set agent_type on each [model.X] entry instead."
);
for entry in resolved.values_mut() {
@ -3548,8 +3603,9 @@ fn apply_global_extra_headers(resolved: &mut IndexMap<String, ModelEntry>, model
return;
}
tracing::debug!(
header_keys = ? models.extra_headers.keys().collect::< Vec < _ >> (), model_count
= resolved.len(), "applying global [models].extra_headers default to all models"
header_keys = ?models.extra_headers.keys().collect::<Vec<_>>(),
model_count = resolved.len(),
"applying global [models].extra_headers default to all models"
);
for entry in resolved.values_mut() {
for (k, v) in &models.extra_headers {
@ -4322,11 +4378,7 @@ where
let value = Option::<toml::Value>::deserialize(deserializer)?;
Ok(value.and_then(|v| {
v.try_into()
.map_err(|e| {
tracing::warn!(
error = % e, "[goal] role model: dropped malformed value"
)
})
.map_err(|e| tracing::warn!(error = %e, "[goal] role model: dropped malformed value"))
.ok()
}))
}
@ -4346,9 +4398,7 @@ where
.filter_map(|v| {
v.try_into()
.map_err(|e| {
tracing::warn!(
error = % e, "[goal] skeptic model: dropped malformed entry"
);
tracing::warn!(error = %e, "[goal] skeptic model: dropped malformed entry");
})
.ok()
})
@ -4428,6 +4478,9 @@ pub struct AutoModeConfig {
/// session model. Resolved via `resolve_aux_model_sampling_config`.
#[serde(skip_serializing_if = "Option::is_none")]
pub classifier_model: Option<String>,
/// Classifier side-query duration in milliseconds; resolved with bounded defaults.
#[serde(skip_serializing_if = "Option::is_none")]
pub classify_timeout_ms: Option<u64>,
/// Classifier reasoning effort. Applies on BOTH the routed-model path and the
/// inherited session-model path; `None` ⇒ the wire fn's built-in default
/// (`low` if the effective model supports reasoning effort, else unset).
@ -4485,7 +4538,10 @@ pub struct Features {
/// compaction. `None` = defer to remote settings / env / default (`false`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub two_pass_compaction: Option<bool>,
/// Video generation tool. `None` = defer to remote settings / env / default (false).
/// `image_gen` / `/imagine`. `None` = env / remote / default (`true`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub image_gen: Option<bool>,
/// Video tools / `/imagine-video`. `None` = env / remote / default (`true`).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub video_gen: Option<bool>,
/// `image_gen` Imagine model override. `None`/empty = defer to remote settings
@ -4662,7 +4718,8 @@ pub fn resolve_credentials(model: &ModelEntry, session_key: Option<&str>) -> Res
&& !env_keys.is_empty()
{
tracing::warn!(
model = % info.model, env_key = % env_keys,
model = %info.model,
env_key = %env_keys,
"model has env_key configured but none of the environment variables are set — \
requests will have no API key",
);
@ -4675,7 +4732,9 @@ pub fn resolve_credentials(model: &ModelEntry, session_key: Option<&str>) -> Res
};
let auth_scheme = info.auth_scheme;
tracing::debug!(
model = % info.model, auth_type = ? auth_type, "resolved credentials"
model = %info.model,
auth_type = ?auth_type,
"resolved credentials"
);
ResolvedCredentials {
api_key,
@ -4701,10 +4760,10 @@ pub fn enforce_disable_api_key_auth(
xai_grok_telemetry::unified_log::debug(
"auth: kill switch blocked a first-party API key at the credential seam",
None,
Some(serde_json::json!(
{ "replaced_with_session" : session_key.is_some(), "base_url" : creds
.base_url, }
)),
Some(serde_json::json!({
"replaced_with_session": session_key.is_some(),
"base_url": creds.base_url,
})),
);
}
}
@ -4730,10 +4789,10 @@ pub fn try_resolve_model_credentials(
session_key: Option<&str>,
) -> Option<ResolvedCredentials> {
let raw = crate::config::load_effective_config()
.map_err(|e| tracing::warn!(error = % e, "config load failed for credential resolution"))
.map_err(|e| tracing::warn!(error = %e, "config load failed for credential resolution"))
.ok()?;
let cfg = Config::new_from_toml_cfg(&raw)
.map_err(|e| tracing::warn!(error = % e, "config parse failed for credential resolution"))
.map_err(|e| tracing::warn!(error = %e, "config parse failed for credential resolution"))
.ok()?;
let models = resolve_model_list(&cfg, None);
let entry = find_model_by_id(&models, model_id)?;
@ -4802,13 +4861,13 @@ enum ModelLookup<'a> {
/// stay conservative on a transient config failure.
fn with_resolved_model<T>(model_id: &str, f: impl FnOnce(ModelLookup) -> T) -> T {
let Some(raw) = crate::config::load_effective_config()
.map_err(|e| tracing::warn!(error = % e, "config load failed for model auth lookup"))
.map_err(|e| tracing::warn!(error = %e, "config load failed for model auth lookup"))
.ok()
else {
return f(ModelLookup::ConfigUnavailable);
};
let Some(cfg) = Config::new_from_toml_cfg(&raw)
.map_err(|e| tracing::warn!(error = % e, "config parse failed for model auth lookup"))
.map_err(|e| tracing::warn!(error = %e, "config parse failed for model auth lookup"))
.ok()
else {
return f(ModelLookup::ConfigUnavailable);
@ -4845,7 +4904,7 @@ pub fn resolve_aux_model_sampling_config(
}
if entry.effective_auth_provider().is_some() {
tracing::warn!(
model = % model_id,
model = %model_id,
"aux model uses an auth provider with no cached token; the caller falls back to its session default"
);
return None;
@ -4908,7 +4967,7 @@ pub fn resolve_aux_model_sampling_config(
return Some(sampler);
}
tracing::warn!(
aux_model = % model_id,
aux_model = %model_id,
"no credentials for auxiliary model; falling back to active model",
);
None
@ -5149,7 +5208,7 @@ pub fn resolve_web_search_sampling_config(
let credentials = resolve_credentials_enforced(&entry, session_key, disable_api_key_auth);
if credentials.api_key.is_none() && entry.effective_auth_provider().is_some() {
tracing::warn!(
web_search_model = % model_id,
web_search_model = %model_id,
"web search model uses an auth provider with no cached token; disabling web search"
);
return None;
@ -5176,7 +5235,7 @@ pub fn resolve_web_search_sampling_config(
};
if resolved.is_none() {
tracing::warn!(
web_search_model = % model_id,
web_search_model = %model_id,
"configured web_search model not found; disabling web search"
);
}
@ -5314,14 +5373,22 @@ mod tests {
enabled = true
prompt_type = "no_user_tool_prefix"
classifier_model = "grok-4.5"
classify_timeout_ms = 45000
reasoning_effort = "low"
"#;
let from_toml: AutoModeConfig = toml::from_str(toml_src).unwrap();
let json = serde_json::json!(
{ "enabled" : true, "prompt_type" : "no_user_tool_prefix", "classifier_model"
: "grok-4.5", "reasoning_effort" : "low" }
);
let json = serde_json::json!({
"enabled": true,
"prompt_type": "no_user_tool_prefix",
"classifier_model": "grok-4.5",
"classify_timeout_ms": 45000,
"reasoning_effort": "low"
});
let from_json: AutoModeConfig = serde_json::from_value(json).unwrap();
assert_eq!(
serde_json::to_value(&from_toml).unwrap(),
serde_json::to_value(&from_json).unwrap()
);
for cfg in [&from_toml, &from_json] {
assert_eq!(cfg.enabled, Some(true));
assert_eq!(
@ -5329,11 +5396,11 @@ reasoning_effort = "low"
Some(ClassifierPromptType::NoUserToolPrefix)
);
assert_eq!(cfg.classifier_model.as_deref(), Some("grok-4.5"));
assert_eq!(cfg.classify_timeout_ms, Some(45_000));
assert_eq!(cfg.reasoning_effort, Some(ReasoningEffort::Low));
}
let empty: AutoModeConfig = toml::from_str("").unwrap();
assert!(empty.enabled.is_none() && empty.prompt_type.is_none());
assert!(empty.classifier_model.is_none() && empty.reasoning_effort.is_none());
assert_eq!(serde_json::to_value(&empty).unwrap(), serde_json::json!({}));
}
/// `prompt_type` wire values are the snake_case `ClassifierPromptType` names.
#[test]
@ -5366,10 +5433,11 @@ reasoning_effort = "low"
}
#[test]
fn laziness_detector_absent_block_deserializes_to_default() {
let json = serde_json::json!(
{ "model" : "test", "base_url" : "https://test.api/v1", "context_window" :
200_000, }
);
let json = serde_json::json!({
"model": "test",
"base_url": "https://test.api/v1",
"context_window": 200_000,
});
let entry: ModelEntryConfig =
serde_json::from_value(json).expect("ModelEntryConfig deserializes without detector");
assert_eq!(
@ -5391,10 +5459,13 @@ reasoning_effort = "low"
}
#[test]
fn laziness_detector_block_round_trips_through_serde() {
let json = serde_json::json!(
{ "enabled" : true, "max_nudges_per_session" : 3, "idle_threshold_ms" :
15_000, "min_confidence" : 0.8, "include_reasoning" : false, }
);
let json = serde_json::json!({
"enabled": true,
"max_nudges_per_session": 3,
"idle_threshold_ms": 15_000,
"min_confidence": 0.8,
"include_reasoning": false,
});
let cfg: LazinessDetectorPerModelConfig =
serde_json::from_value(json).expect("deserialize populated block");
assert!(cfg.enabled);
@ -5411,11 +5482,11 @@ reasoning_effort = "low"
#[test]
fn laziness_detector_include_reasoning_serde_states() {
let some_true: LazinessDetectorPerModelConfig =
serde_json::from_value(serde_json::json!({ "include_reasoning" : true }))
serde_json::from_value(serde_json::json!({ "include_reasoning": true }))
.expect("Some(true)");
assert_eq!(some_true.include_reasoning, Some(true));
let some_false: LazinessDetectorPerModelConfig =
serde_json::from_value(serde_json::json!({ "include_reasoning" : false }))
serde_json::from_value(serde_json::json!({ "include_reasoning": false }))
.expect("Some(false)");
assert_eq!(some_false.include_reasoning, Some(false));
let absent: LazinessDetectorPerModelConfig =
@ -5869,9 +5940,9 @@ reasoning_effort = "low"
cfg.config_warnings.iter().any(|w| {
w.kind == kind
&& matches!(
& w.target, WarningTarget::AuthProvider { name : n, field : f
}
if n == name && f.as_deref() == field
&w.target,
WarningTarget::AuthProvider { name: n, field: f }
if n == name && f.as_deref() == field
)
})
};
@ -5909,10 +5980,8 @@ if n == name && f.as_deref() == field
cfg.config_warnings
.iter()
.find(|w| {
matches!(
& w.target, WarningTarget::AuthProvider { name : n, field : f }
if n == name && f.as_deref() == Some("timeout_secs")
)
matches!(&w.target, WarningTarget::AuthProvider { name: n, field: f }
if n == name && f.as_deref() == Some("timeout_secs"))
})
.map(|w| w.reason.as_str())
.unwrap_or_default()
@ -5923,8 +5992,10 @@ if n == name && f.as_deref() == field
assert!(
cfg.config_warnings.iter().any(|w| {
w.kind == ConfigWarningKind::InvalidValue
&& matches!(& w.target, WarningTarget::Model
{ field, .. } if field.as_deref() == Some("auth_provider"))
&& matches!(
&w.target,
WarningTarget::Model { field, .. } if field.as_deref() == Some("auth_provider")
)
}),
"undefined reference warns at parse time: {:?}",
cfg.config_warnings
@ -9126,15 +9197,17 @@ if n == name && f.as_deref() == field
}
#[test]
#[serial]
fn background_workflows_default_off_without_affecting_goal() {
fn background_workflows_default_on_without_affecting_goal() {
unsafe { std::env::remove_var("GROK_WORKFLOWS") };
let cfg = Config::default();
assert!(!cfg.resolve_workflows().value);
let r = cfg.resolve_workflows();
assert!(r.value);
assert_eq!(r.source, ConfigSource::Default);
assert!(cfg.resolve_goal().value);
}
#[test]
#[serial]
fn resolve_workflows_remote_settings_opt_in() {
fn resolve_workflows_remote_settings_enables() {
unsafe { std::env::remove_var("GROK_WORKFLOWS") };
let cfg = Config {
remote_settings: Some(crate::util::config::RemoteSettings {
@ -9165,11 +9238,14 @@ if n == name && f.as_deref() == field
#[test]
#[serial]
fn resolve_workflows_env_wins() {
unsafe { std::env::set_var("GROK_WORKFLOWS", "1") };
unsafe { std::env::set_var("GROK_WORKFLOWS", "0") };
let cfg = Config::default();
let r = cfg.resolve_workflows();
assert_eq!(r.source, ConfigSource::Env);
assert!(r.value);
assert!(
!r.value,
"env must be able to kill the default-on workflows"
);
unsafe { std::env::remove_var("GROK_WORKFLOWS") };
}
#[test]
@ -9288,6 +9364,85 @@ if n == name && f.as_deref() == field
assert!(with_list(vec!["image_to_video"]).resolve_image_edit().value);
assert!(Config::default().resolve_image_edit().value);
}
#[test]
#[serial]
fn resolve_image_gen_gates() {
unsafe { std::env::remove_var("GROK_IMAGE_GEN") };
assert!(Config::default().resolve_image_gen().value);
assert!(
!Config {
features: Features {
image_gen: Some(false),
..Default::default()
},
..Default::default()
}
.resolve_image_gen()
.value
);
assert!(
!Config {
remote_settings: Some(crate::util::config::RemoteSettings {
image_gen_enabled: Some(false),
..Default::default()
}),
..Default::default()
}
.resolve_image_gen()
.value
);
unsafe { std::env::set_var("GROK_IMAGE_GEN", "1") };
let denied = Config {
remote_settings: Some(crate::util::config::RemoteSettings {
imagine_tools_disabled: Some(vec!["image_gen".into()]),
..Default::default()
}),
..Default::default()
}
.resolve_image_gen();
assert!(!denied.value);
assert_eq!(denied.source, ConfigSource::Remote);
unsafe { std::env::remove_var("GROK_IMAGE_GEN") };
}
#[test]
#[serial]
fn resolve_video_gen_gates() {
unsafe { std::env::remove_var("GROK_VIDEO_GEN") };
assert!(Config::default().resolve_video_gen().value);
assert!(
!Config {
features: Features {
video_gen: Some(false),
..Default::default()
},
..Default::default()
}
.resolve_video_gen()
.value
);
assert!(
!Config {
remote_settings: Some(crate::util::config::RemoteSettings {
video_gen_enabled: Some(false),
..Default::default()
}),
..Default::default()
}
.resolve_video_gen()
.value
);
assert!(
!Config {
remote_settings: Some(crate::util::config::RemoteSettings {
imagine_tools_disabled: Some(vec!["image_to_video".into()]),
..Default::default()
}),
..Default::default()
}
.resolve_video_gen()
.value
);
}
/// Clear every env var the goal/companion resolvers read so tests
/// start from a known baseline regardless of run order.
fn clear_goal_envs() {

View file

@ -658,8 +658,7 @@ mod tests {
assert_eq!(warnings[0].kind, ConfigWarningKind::NotATable);
assert!(matches!(
&warnings[0].target,
WarningTarget::Model { key, field: None }
if key == "oops"
WarningTarget::Model { key, field: None } if key == "oops"
));
}

View file

@ -1,12 +1,14 @@
//! Folder-trust gate ("do you trust this folder?").
//!
//! Repo-local MCP / LSP servers are configured by files an attacker can ship
//! inside a cloned repository (`.mcp.json`, project `.grok/config.toml`,
//! `~/.claude.json` `projects.<cwd>`, project `.grok/lsp.json`). Those configs
//! contain commands that the CLI would otherwise spawn automatically — a
//! 1-click RCE. This module resolves a VS-Code-style trust decision ONCE per
//! workspace, BEFORE any repo-local server is spawned, and exposes a cheap
//! [`project_scope_allowed`] check that the MCP/LSP loaders consult.
//! Repo-local MCP / LSP servers and permission policy are configured by files
//! an attacker can ship inside a cloned repository (`.mcp.json`, project
//! `.grok/config.toml` including `[permission]` / `[mcp_servers]` /
//! `[plugins].paths`, `~/.claude.json` `projects.<cwd>`, project `.grok/lsp.json`).
//! Those configs contain commands or auto-approve rules the CLI would otherwise
//! honor automatically — a 1-click RCE / policy bypass. This module resolves a
//! VS-Code-style trust decision ONCE per workspace, BEFORE any repo-local
//! server is spawned, and exposes a cheap [`project_scope_allowed`] check that
//! the MCP/LSP/permission loaders consult.
//!
//! Resolution lives here (not in `acp_session`) so the session core stays free
//! of feature logic; the loaders only call [`project_scope_allowed`].
@ -971,6 +973,34 @@ mod tests {
);
}
#[test]
#[serial_test::serial]
fn project_scope_allowed_denies_untrusted_permission_only_repo() {
// Bridge: a clone whose ONLY repo-local config is `.grok/config.toml`
// `[permission]` (no MCP/hooks/plugins) must still produce untrusted via
// the real `repo_configs_present` → `decide` → `project_scope_allowed`
// path. Resolver unit tests inject `project_trusted = false` directly and
// miss this detector gap. Subdir launch ensures the cwd→git-root walk.
let _sim = simulate_release_build();
let home = tempfile::tempdir().unwrap();
let _env = EnvGuard::set("GROK_HOME", home.path());
let _flag = EnvGuard::unset("GROK_FOLDER_TRUST");
let tmp = repo_tmp();
let grok = tmp.path().join(".grok");
std::fs::create_dir_all(&grok).unwrap();
std::fs::write(
grok.join("config.toml"),
"[permission]\nallow = [\"Bash(*)\"]\n",
)
.unwrap();
let subdir = tmp.path().join("crates").join("inner");
std::fs::create_dir_all(&subdir).unwrap();
assert!(
!project_scope_allowed(&subdir),
"permission-only untrusted repo must be denied from a subdirectory"
);
}
#[test]
#[serial_test::serial]
fn kill_switch_allows_untrusted_repo_after_authoritative_resolve() {

View file

@ -18,7 +18,7 @@ pub(crate) async fn apply(
xai_grok_telemetry::unified_log::info(
"model changed",
Some(args.session_id.0.as_ref()),
Some(serde_json::json!({ "model" : args.model_id.0.as_ref() })),
Some(serde_json::json!({"model": args.model_id.0.as_ref()})),
);
tracing::debug!("session_session_model::mvp_agent: {:?}", &args);
let effort_override = parse_reasoning_effort_meta(args.meta.as_ref());
@ -58,14 +58,21 @@ pub(crate) async fn apply(
.as_ref()
.is_some_and(|active| !harnesses_are_compatible(active, required));
tracing::info!(
session_id = % session_id.0, model_id = % model_id.0, ? required_agent_type,
? active_agent_type, turn_count, is_mismatch,
session_id = %session_id.0,
model_id = %model_id.0,
?required_agent_type,
?active_agent_type,
turn_count,
is_mismatch,
"set_session_model: agent type compatibility check"
);
if is_mismatch && turn_count > 0 {
tracing::warn!(
session_id = % session_id.0, model_id = % model_id.0, active_agent = ?
active_agent_type, required_agent = % required, turn_count,
session_id = %session_id.0,
model_id = %model_id.0,
active_agent = ?active_agent_type,
required_agent = %required,
turn_count,
"set_session_model: agent type mismatch rejected"
);
xai_grok_telemetry::session_ctx::log_event(xai_grok_telemetry::events::ModelSwitched {
@ -96,16 +103,19 @@ pub(crate) async fn apply(
match resolved {
Some(def) => {
tracing::info!(
session_id = % session_id.0, model_id = % model_id.0,
required_agent_type = % required, agent_def_name = % def.name,
session_id = %session_id.0,
model_id = %model_id.0,
required_agent_type = %required,
agent_def_name = %def.name,
"set_session_model: zero-turn harness switch — queued agent rebuild"
);
pending_rebuild_definition = Some(def);
}
None => {
tracing::warn!(
session_id = % session_id.0, model_id = % model_id.0,
required_agent_type = % required,
session_id = %session_id.0,
model_id = %model_id.0,
required_agent_type = %required,
"set_session_model: zero-turn harness switch — could not resolve agent definition; proceeding with stale harness"
);
}
@ -120,13 +130,16 @@ pub(crate) async fn apply(
.model_supports_reasoning_effort(model_id.0.as_ref())
{
tracing::info!(
session_id = % session_id.0, effort = % eff,
session_id = %session_id.0,
effort = %eff,
"set_session_model: applying reasoning_effort override from meta"
);
model_sampling.reasoning_effort = Some(eff);
} else {
tracing::warn!(
session_id = % session_id.0, model_id = % model_id.0, effort = % eff,
session_id = %session_id.0,
model_id = %model_id.0,
effort = %eff,
"set_session_model: ignoring reasoning_effort override — model does not support it"
);
}
@ -138,7 +151,8 @@ pub(crate) async fn apply(
let apply_prompt_override = !gate_closed;
if gate_closed {
tracing::info!(
session_id = % session_id.0, model_id = % model_id.0,
session_id = %session_id.0,
model_id = %model_id.0,
"set_session_model: gateway gate closed, prompt override suppressed"
);
pending_rebuild_definition = None;
@ -158,7 +172,9 @@ pub(crate) async fn apply(
Ok(()) => true,
Err(e) => {
tracing::error!(
session_id = % session_id.0, model_id = % model_id.0, error = ? e,
session_id = %session_id.0,
model_id = %model_id.0,
error = ?e,
"set_session_model: zero-turn harness rebuild failed; aborting model switch"
);
xai_grok_telemetry::session_ctx::log_event(
@ -230,9 +246,11 @@ pub(crate) async fn apply(
}
agent.sync_process_static_api_key(Some(model_id.0.as_ref()));
Ok(acp::SetSessionModelResponse::new().meta(
serde_json::json!({ "model" : updated_model, })
.as_object()
.cloned(),
serde_json::json!({
"model": updated_model,
})
.as_object()
.cloned(),
))
}
/// Broadcast a `ModelChanged` to every client subscribed to this session so

View file

@ -319,7 +319,7 @@ impl MvpAgent {
{
Ok(servers) => servers,
Err(e) => {
tracing::warn!(error = % e, "initialize MCP setup task failed");
tracing::warn!(error = %e, "initialize MCP setup task failed");
return;
}
};
@ -378,7 +378,8 @@ impl MvpAgent {
.plugin_registry_handle
.reload(Some(cwd), &disk_config, trusted, false);
tracing::debug!(
plugin_count = count, "lazily populated plugin registry snapshot"
plugin_count = count,
"lazily populated plugin registry snapshot"
);
}
/// Fetch managed configs, merge with client servers, return merged list + earliest expiry.
@ -437,7 +438,7 @@ impl MvpAgent {
&& tx.send(cwd.to_path_buf()).is_err()
{
tracing::debug!(
cwd = % cwd.display(),
cwd = %cwd.display(),
"config watcher path channel closed; session cwd not registered"
);
}
@ -676,7 +677,10 @@ impl MvpAgent {
) {
Ok(handle) => xai_grok_workspace::WorkspaceOps::local(handle),
Err(e) => {
tracing::error!(error = % e, "failed to create local WorkspaceHandle");
tracing::error!(
error = %e,
"failed to create local WorkspaceHandle"
);
return Err(
acp::Error::internal_error().data("workspace not initialized"),
);
@ -766,30 +770,28 @@ impl MvpAgent {
}
_ => auth_method::PREFERRED_OIDC_UNAVAILABLE,
};
tracing::info!(
% msg, "cached_token unavailable; preferred_method forbids fallthrough"
);
tracing::info!(%msg, "cached_token unavailable; preferred_method forbids fallthrough");
xai_grok_telemetry::unified_log::warn(
"auth cached_token fallthrough blocked by preferred_method",
None,
Some(
serde_json::json!(
{ "preferred_method" : preferred.map(| p | format!("{p:?}")), }
),
serde_json::json!({
"preferred_method": preferred.map(|p| format!("{p:?}")),
}),
),
);
return Err(acp::Error::auth_required().data(msg));
};
let meta = if method_id.0.as_ref() == auth_method::GROK_COM_METHOD_ID {
serde_json::json!({ "use_oauth" : true }).as_object().cloned()
serde_json::json!({ "use_oauth": true }).as_object().cloned()
} else {
arguments.meta
};
tracing::info!(fallback = % method_id.0, "cached_token fallthrough");
tracing::info!(fallback = %method_id.0, "cached_token fallthrough");
xai_grok_telemetry::unified_log::warn(
"auth cached_token fallthrough",
None,
Some(serde_json::json!({ "fallback" : method_id.0.as_ref() })),
Some(serde_json::json!({ "fallback": method_id.0.as_ref() })),
);
acp::Agent::authenticate(
self,
@ -847,7 +849,8 @@ impl MvpAgent {
let telemetry_mode = cfg.resolve_telemetry_mode();
let trace_upload = cfg.resolve_trace_upload();
tracing::info!(
telemetry = % telemetry_mode, trace_upload = % trace_upload,
telemetry = %telemetry_mode,
trace_upload = %trace_upload,
"post-auth data capture config re-resolved",
);
let grok_user_id = is_xai.then(|| user_id.clone());
@ -908,9 +911,7 @@ impl MvpAgent {
crate::util::config::sync_campaign_fields(&mut cfg);
let raw_config = crate::config::load_effective_config()
.unwrap_or_else(|e| {
tracing::warn!(
error = % e, "config reload failed during settings refresh"
);
tracing::warn!(error = %e, "config reload failed during settings refresh");
toml::Value::Table(toml::map::Map::new())
});
cfg.re_resolve_runtime_fields(&raw_config);
@ -994,9 +995,7 @@ impl MvpAgent {
return;
};
if stored.announcements != pre_fetch {
tracing::debug!(
"announcements poll apply skipped: settings changed mid-fetch"
);
tracing::debug!("announcements poll apply skipped: settings changed mid-fetch");
return;
}
stored.announcements = fresh.announcements;
@ -1024,9 +1023,10 @@ impl MvpAgent {
let Some(announcements) = payload_list else {
return;
};
let payload = serde_json::json!(
{ "gen" : self.next_announcements_gen(), "announcements" : announcements, }
);
let payload = serde_json::json!({
"gen": self.next_announcements_gen(),
"announcements": announcements,
});
let Ok(params) = serde_json::value::to_raw_value(&payload) else {
return;
};
@ -1040,7 +1040,8 @@ impl MvpAgent {
}
*self.last_emitted_announcements.borrow_mut() = announcements.clone();
tracing::info!(
count = announcements.len(), mode = ? mode,
count = announcements.len(),
mode = ?mode,
"pushing announcements update to clients"
);
}
@ -1083,7 +1084,7 @@ impl MvpAgent {
{
Ok(settings) => settings,
Err(e) => {
tracing::warn!(error = % e, "settings fetch task panicked");
tracing::warn!(error = %e, "settings fetch task panicked");
None
}
}
@ -1122,7 +1123,8 @@ impl MvpAgent {
let models = self.models_manager.models();
let Some(catalog_key) = resolve_catalog_key(&models, requested) else {
tracing::debug!(
requested = % requested_str, model_count = models.len(),
requested = %requested_str,
model_count = models.len(),
"resolve_model_id: unknown model id (not in models() by key or .model field)"
);
return Err(acp::Error::invalid_params().data("unknown model id"));
@ -1136,8 +1138,10 @@ impl MvpAgent {
"model field scan"
};
tracing::debug!(
"resolve_model_id: matched by {}: requested={} model={}", match_kind,
requested_str, entry.info.model
"resolve_model_id: matched by {}: requested={} model={}",
match_kind,
requested_str,
entry.info.model
);
Ok(entry.clone())
}
@ -1157,7 +1161,7 @@ impl MvpAgent {
model,
session.as_ref().map(|a| a.key.as_str()),
);
if matches!(preferred, Some(crate ::auth::PreferredAuthMethod::Oidc))
if matches!(preferred, Some(crate::auth::PreferredAuthMethod::Oidc))
&& !model.has_own_credentials()
&& credentials.auth_type == xai_chat_state::AuthType::ApiKey
{
@ -1179,25 +1183,26 @@ impl MvpAgent {
xai_grok_telemetry::unified_log::info(
"auth auth_type override to SessionToken",
None,
Some(serde_json::json!({ "model" : model.info().model.as_str() })),
Some(serde_json::json!({ "model": model.info().model.as_str() })),
);
credentials.auth_type = xai_chat_state::AuthType::SessionToken;
}
if !has_session_key && !model.has_own_credentials() {
tracing::warn!(
model = model.info().model.as_str(), is_expired = self.auth_manager
.is_expired(), auth_type = ? credentials.auth_type,
model = model.info().model.as_str(),
is_expired = self.auth_manager.is_expired(),
auth_type = ?credentials.auth_type,
"auth: prepare_sampling_config has no session key",
);
xai_grok_telemetry::unified_log::warn(
"auth: prepare_sampling_config has no session key",
None,
Some(
serde_json::json!(
{ "model" : model.info().model.as_str(), "is_expired" : self
.auth_manager.is_expired(), "auth_type" : format!("{:?}",
credentials.auth_type), }
),
serde_json::json!({
"model": model.info().model.as_str(),
"is_expired": self.auth_manager.is_expired(),
"auth_type": format!("{:?}", credentials.auth_type),
}),
),
);
}
@ -1259,7 +1264,8 @@ impl MvpAgent {
};
let new_config = self.prepare_sampling_config_for_model(model, origin_client);
tracing::info!(
model = % id.0, "agent profile model override applied to parent session"
model = %id.0,
"agent profile model override applied to parent session"
);
(id.clone(), new_config)
}
@ -1348,11 +1354,14 @@ impl MvpAgent {
&self,
) -> xai_grok_tools::implementations::grok_build::video_gen::VideoGenConfig {
use xai_grok_tools::implementations::grok_build::video_gen::VideoGenConfig;
let cfg = self.cfg.borrow();
if !cfg.resolve_video_gen().value {
return VideoGenConfig::Disabled;
}
let Some(api_key) = self.sampling_config.borrow().api_key.clone() else {
return VideoGenConfig::Disabled;
};
let tier_restricted = self.is_tier_restricted_capability();
let cfg = self.cfg.borrow();
let zdr_video_output_s3 = cfg
.disable_zdr_incompatible_tools
.then(|| cfg.zdr_video_output_s3.clone())
@ -1503,25 +1512,24 @@ impl MvpAgent {
config_root.as_ref(),
);
tracing::info!(
worktree_type = ? worktree_type, source = wt_source,
worktree_type = ?worktree_type,
source = wt_source,
"WORKTREE_CONFIG_SHELL: resolved worktree type at agent startup"
);
if relay_sync_enabled {
tracing::info!("[grok] Relay sync: ENABLED");
} else if tui_mode && relay_config_enabled && !has_xai_auth {
tracing::info!(
"[grok] Relay sync: DISABLED (no auth - run 'grok login' first)"
);
tracing::info!("[grok] Relay sync: DISABLED (no auth - run 'grok login' first)");
} else if tui_mode && !relay_config_enabled {
tracing::debug!(
"Relay sync: DISABLED (not configured in config.toml or env)"
);
tracing::debug!("Relay sync: DISABLED (not configured in config.toml or env)");
} else {
tracing::debug!("Relay sync: DISABLED (not in TUI mode)");
}
if cfg.telemetry.trace_upload == Some(false) {
tracing::info!(
enabled = false, reason = "feature_off", "trace_upload_status"
enabled = false,
reason = "feature_off",
"trace_upload_status"
);
}
let (subagent_event_tx, subagent_event_rx) = tokio::sync::mpsc::unbounded_channel();
@ -1675,7 +1683,8 @@ impl MvpAgent {
return;
}
tracing::info!(
count = p.session_ids.len(), sessions = ? p.session_ids,
count = p.session_ids.len(),
sessions = ?p.session_ids,
"Client disconnected; detaching sessions (no-evict keystone)"
);
let checks = p
@ -1696,7 +1705,7 @@ impl MvpAgent {
self.set_session_live_state(&id, SessionLiveState::Working);
kept_resident += 1;
tracing::info!(
session_id = % id.0,
session_id = %id.0,
"kept session resident across client disconnect (live work)"
);
continue;
@ -1707,9 +1716,7 @@ impl MvpAgent {
self.require_gateway_sessions.borrow_mut().remove(&id);
self.set_session_live_state(&id, SessionLiveState::Dormant);
unloaded += 1;
tracing::debug!(
session_id = % id.0, "idle session unloaded to disk on disconnect"
);
tracing::debug!(session_id = %id.0, "idle session unloaded to disk on disconnect");
}
}
tracing::info!(kept_resident, unloaded, "client-disconnect detach complete");
@ -1733,20 +1740,21 @@ impl MvpAgent {
return;
}
tracing::info!(
session_id = % session_id.0,
session_id = %session_id.0,
"Waiting for old session thread to finish before reload"
);
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
if thread.is_finished() {
tracing::debug!(
session_id = % session_id.0, "Old session thread finished cleanly"
session_id = %session_id.0,
"Old session thread finished cleanly"
);
return;
}
if tokio::time::Instant::now() >= deadline {
tracing::warn!(
session_id = % session_id.0,
session_id = %session_id.0,
"Old session thread still running after 5s — proceeding with replay. \
Session data may be incomplete if the old actor is still writing."
);
@ -1824,7 +1832,7 @@ impl MvpAgent {
let now = tokio::time::Instant::now();
if now >= deadline {
tracing::warn!(
session_id = % session_id.0,
session_id = %session_id.0,
"timed out waiting for in-flight session/load"
);
return;
@ -2107,7 +2115,7 @@ impl MvpAgent {
let handle = self.get_session_handle(session_id)?;
let outcome = handle.execute_plugins_action(action).await;
let succeeded = matches!(
outcome.as_ref().map(| o | & o.status),
outcome.as_ref().map(|o| &o.status),
Some(xai_hooks_plugins_types::OutcomeStatus::Success)
);
if is_reload && succeeded {
@ -2397,7 +2405,7 @@ impl MvpAgent {
model_state.current_model_id.0.to_string(),
title,
);
(serde_json::json!({ "options" : config_options }), serde_json::json!(detail))
(serde_json::json!({ "options": config_options }), serde_json::json!(detail))
}
/// Seed the global sampling config with login auth when available.
///
@ -2422,9 +2430,7 @@ impl MvpAgent {
.values()
.any(|m| m.has_own_credentials())
{
tracing::warn!(
"No credentials found: no login token and no model api_key/env_key"
);
tracing::warn!("No credentials found: no login token and no model api_key/env_key");
xai_grok_telemetry::unified_log::warn(
"No credentials found: no login token and no model api_key/env_key",
None,
@ -2543,10 +2549,10 @@ impl MvpAgent {
&capture.messages,
);
futures::join!(
upload_metadata(& ctx, metadata), upload_turn_messages(& ctx,
capture, UploadWait::Confirm), upload_harness_session_archive(&
ctx, session_state),
);
upload_metadata(&ctx, metadata),
upload_turn_messages(&ctx, capture, UploadWait::Confirm),
upload_harness_session_archive(&ctx, session_state),
);
let upload_method = resolve_upload_method(&ctx);
write_upload_manifest(
&ctx,
@ -2769,13 +2775,14 @@ impl MvpAgent {
&& let Some(def) = xai_grok_agent::discovery::by_name_in_cwd(required, cwd)
{
tracing::info!(
agent_name = % def.name, "Using agent definition from model agent_type"
agent_name = %def.name,
"Using agent definition from model agent_type"
);
return def;
}
if let Some(def) = acp_agent_profile {
tracing::info!(
agent_name = % def.name,
agent_name = %def.name,
"Using agent profile from ACP _meta.agentProfile"
);
return def;
@ -2785,11 +2792,14 @@ impl MvpAgent {
Ok(def) => return def,
Err(e) => {
tracing::error!(
path = % path.display(), error = % e,
path = %path.display(),
error = %e,
"Failed to load agent profile from --agent-profile path"
);
eprintln!(
"error: failed to load agent profile '{}': {}", path.display(), e
"error: failed to load agent profile '{}': {}",
path.display(),
e
);
crate::instrumentation::finalize_and_exit(1);
}
@ -2799,14 +2809,16 @@ impl MvpAgent {
match AgentDefinition::from_file(path) {
Ok(def) => {
tracing::info!(
agent_name = % def.name, path = % path.display(),
agent_name = %def.name,
path = %path.display(),
"Using agent definition from config.toml [agent] definition"
);
return def;
}
Err(e) => {
tracing::warn!(
path = % path.display(), error = % e,
path = %path.display(),
error = %e,
"Failed to load agent definition from config.toml [agent] definition, \
falling through to next source"
);
@ -2815,14 +2827,14 @@ impl MvpAgent {
}
if let Some(ref name) = agent_config.name {
tracing::info!(
agent_name = % name,
agent_name = %name,
"Resolving agent definition from config.toml [agent] name"
);
if let Some(def) = xai_grok_agent::discovery::by_name_in_cwd(name, cwd) {
return def;
}
tracing::warn!(
agent_name = % name,
agent_name = %name,
"Agent '{}' not found via discovery, falling through to next source",
name
);
@ -2838,7 +2850,8 @@ impl MvpAgent {
Ok(def) => def,
Err(e) => {
tracing::warn!(
path = path, error = % e,
path = path,
error = %e,
"Failed to load agent definition from file, falling back to default"
);
AgentDefinition::grok_build_plan()
@ -2856,14 +2869,16 @@ impl MvpAgent {
&& resolved.name != required
{
tracing::info!(
resolved_agent = % resolved.name, model_agent_type = % required,
resolved_agent = %resolved.name,
model_agent_type = %required,
"resolve_agent_definition: model requires different agent, re-resolving"
);
if let Some(def) = xai_grok_agent::discovery::by_name_in_cwd(required, cwd) {
return def;
}
tracing::warn!(
model_agent_type = % required, fallback_agent = % resolved.name,
model_agent_type = %required,
fallback_agent = %resolved.name,
"resolve_agent_definition: model agent_type '{}' not found via discovery, \
keeping chain-resolved agent",
required,
@ -3072,7 +3087,7 @@ impl MvpAgent {
};
(resolved, flags)
};
tracing::info!(feedback = % feedback_resolved, "resolved feedback feature flag");
tracing::info!(feedback = %feedback_resolved, "resolved feedback feature flag");
let loc_aggregate_rx = match hunk_event_rx {
Some((hunk_event_rx, loc_cancel)) if loc_tracking_enabled => {
let (loc_agg_tx, loc_agg_rx) = tokio::sync::mpsc::unbounded_channel();
@ -3165,7 +3180,9 @@ impl MvpAgent {
}
let auth_method_id = std::sync::Arc::clone(&self.auth_method_id);
tracing::info!(
session_id = % session_info.id.0, ? startup_hints, "startup hints"
session_id = %session_info.id.0,
?startup_hints,
"startup hints"
);
let auto_compact_threshold_percent = {
let cfg = self.cfg.borrow();
@ -3208,7 +3225,8 @@ impl MvpAgent {
(None, None, None, None)
};
tracing::info!(
session_id = % session_info.id.0, feedback_url = ? feedback_proxy_url,
session_id = %session_info.id.0,
feedback_url = ?feedback_proxy_url,
authenticated = feedback_user_token.is_some(),
"Initializing feedback manager for session"
);
@ -3234,9 +3252,11 @@ impl MvpAgent {
overrides.apply_to_definition(&mut agent_definition);
if overrides.has_definition_overrides() {
tracing::debug!(
agent = % agent_definition.name, tools = ? overrides.tools,
disallowed = ? overrides.disallowed_tools, permission_mode = ?
overrides.permission_mode, "cli agent overrides applied"
agent = %agent_definition.name,
tools = ?overrides.tools,
disallowed = ?overrides.disallowed_tools,
permission_mode = ?overrides.permission_mode,
"cli agent overrides applied"
);
}
}
@ -3249,7 +3269,8 @@ impl MvpAgent {
Ok(entry) => Some((mid, entry)),
Err(_) => {
tracing::warn!(
agent = % agent_definition.name, model = % id,
agent = %agent_definition.name,
model = %id,
"agent profile model not in catalog, keeping session default"
);
None
@ -3264,7 +3285,7 @@ impl MvpAgent {
cwd.as_path(),
) {
tracing::info!(
agent = % agent_definition.name,
agent = %agent_definition.name,
"Inheriting harness wire-format from the profile model's agent_type"
);
agent_definition.user_message_template = template;
@ -3337,8 +3358,9 @@ impl MvpAgent {
.join("lsp.json");
let project_path = tool_ctx.cwd.as_path().join(".grok").join("lsp.json");
tracing::warn!(
cwd = % tool_ctx.cwd, user_lsp_path = % user_path.display(),
project_lsp_path = % project_path.display(),
cwd = %tool_ctx.cwd,
user_lsp_path = %user_path.display(),
project_lsp_path = %project_path.display(),
"LSP tools enabled, but no language servers are configured"
);
} else {
@ -3447,12 +3469,13 @@ impl MvpAgent {
);
if changed {
tracing::info!(
session_id = % session_info.id.0, prompt_len = override_prompt.len(),
session_id = %session_info.id.0,
prompt_len = override_prompt.len(),
"cold-load: applied systemPromptOverride to loaded head"
);
} else {
tracing::debug!(
session_id = % session_info.id.0,
session_id = %session_info.id.0,
"cold-load: systemPromptOverride already matches head, no-op"
);
}
@ -3489,10 +3512,7 @@ impl MvpAgent {
std::path::Path::new(&session_info.cwd),
);
for e in &errors {
tracing::warn!(
agent = % agent_definition.name, error = ? e,
"agent hook parse error"
);
tracing::warn!(agent = %agent_definition.name, error = ?e, "agent hook parse error");
}
if specs.is_empty() {
return None;
@ -3509,7 +3529,7 @@ impl MvpAgent {
hooks_trusted,
);
for e in &disk_errors {
tracing::warn!(error = ? e, "hook loading error");
tracing::warn!(error = ?e, "hook loading error");
}
let mut merged = disk_registry;
if folder_trust::agent_inline_hooks_allowed(
@ -3677,9 +3697,7 @@ impl MvpAgent {
self.session_threads
.borrow_mut()
.insert(session_info.id.clone(), session_thread);
tracing::debug!(
session_id = % session_info.id.0, "spawn_session_on_thread complete"
);
tracing::debug!(session_id = %session_info.id.0, "spawn_session_on_thread complete");
self.set_session_live_state(&session_info.id, SessionLiveState::IdleResident);
self.ensure_session_supervisor();
self.heap_profile_set_session_id(&session_info.id.0);
@ -3691,15 +3709,16 @@ impl MvpAgent {
init_meta,
&agent_system_prompt,
);
tracing::debug!(session_id = % session_info.id.0, "built system prompt");
tracing::debug!(
session_id = %session_info.id.0,
"built system prompt"
);
let _ = handle
.cmd_tx
.send(SessionCommand::Initialize {
system_prompt,
});
tracing::debug!(
session_id = % session_info.id.0, "enqueued SessionCommand::Initialize"
);
tracing::debug!(session_id = %session_info.id.0, "enqueued SessionCommand::Initialize");
}
let _ = handle.cmd_tx.send(SessionCommand::AdvertiseCommands);
if let Some(mut loc_rx) = loc_aggregate_rx {

View file

@ -267,7 +267,7 @@ fn chat_new_session_model_state(
&& !state.available_models.iter().any(|m| m.model_id.0.as_ref() == requested)
{
tracing::warn!(
requested_model = % requested,
requested_model = %requested,
"chat session/new _meta.modelId not in the /rest/modes catalog; \
reporting it as current anyway (picker may diverge from catalog)"
);
@ -294,7 +294,7 @@ pub(crate) fn parse_session_plugin_dirs(
let mut dirs = Vec::new();
for entry in entries {
let Some(raw) = entry.as_str() else {
tracing::warn!(? entry, "pluginDirs entry is not a string; skipping");
tracing::warn!(?entry, "pluginDirs entry is not a string; skipping");
continue;
};
let path = std::path::PathBuf::from(raw);
@ -427,6 +427,8 @@ pub(crate) struct PromptResponseMeta {
pub structured_output: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub structured_output_error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_overrides: Option<xai_grok_sampling_types::ToolOverrides>,
}
/// Inputs for [`build_prompt_response_meta`]. A struct (not positional args)
/// so call sites are self-documenting and adding a field can't silently
@ -441,6 +443,7 @@ pub(crate) struct PromptResponseMetaArgs<'a> {
pub cancellation_category: Option<String>,
pub cancel_trigger: Option<String>,
pub structured_output: Option<Result<serde_json::Value, String>>,
pub tool_overrides: Option<xai_grok_sampling_types::ToolOverrides>,
}
/// Build the `_meta` JSON for `PromptResponse`. Includes baseline
/// session/prompt/model identifiers plus optional per-turn token counts
@ -458,6 +461,7 @@ pub(crate) fn build_prompt_response_meta(
cancellation_category,
cancel_trigger,
structured_output,
tool_overrides,
} = args;
let (structured_output, structured_output_error) = match structured_output {
Some(Ok(value)) => (Some(value), None),
@ -479,6 +483,7 @@ pub(crate) fn build_prompt_response_meta(
cancel_trigger,
structured_output,
structured_output_error,
tool_overrides,
};
serde_json::to_value(meta).expect("PromptResponseMeta is always serializable")
}
@ -491,6 +496,8 @@ pub(crate) fn build_prompt_response_meta(
struct SettingsUpdateNotification {
show_resolved_model: Option<bool>,
sharing_enabled: Option<bool>,
privacy_notice_rollout: Option<bool>,
privacy_banner_reshow_days: Option<u64>,
session_picker_grouped: Option<bool>,
tips: Option<Vec<String>>,
announcements: Option<Vec<xai_grok_announcements::RemoteAnnouncement>>,
@ -1268,8 +1275,12 @@ fn emit_login_span(
error_category: Option<&str>,
) {
let span = tracing::info_span!(
"auth.lifecycle", action = "login", success, auth_method, user_id =
tracing::field::Empty, error_category = tracing::field::Empty,
"auth.lifecycle",
action = "login",
success,
auth_method,
user_id = tracing::field::Empty,
error_category = tracing::field::Empty,
);
if let Some(uid) = user_id
.filter(|u| !u.is_empty() && !u.eq_ignore_ascii_case("unknown"))
@ -1317,7 +1328,7 @@ impl MvpAgent {
let env = match serde_json::from_str::<RawLinePeek<'_>>(line) {
Ok(e) => e,
Err(e) => {
tracing::debug!(? e, "replay: skipping unparseable JSONL line");
tracing::debug!(?e, "replay: skipping unparseable JSONL line");
return;
}
};
@ -1348,9 +1359,7 @@ impl MvpAgent {
let Ok(mut params) = serde_json::from_str::<
serde_json::Value,
>(raw_params.get()) else {
tracing::debug!(
"replay: skipping xAI update with unparseable params"
);
tracing::debug!("replay: skipping xAI update with unparseable params");
return;
};
if let Some(obj) = params.as_object_mut() {
@ -1393,8 +1402,8 @@ impl MvpAgent {
match &mut notification.update {
acp::SessionUpdate::ToolCall(tc) => {
let is_pre_completed = matches!(
tc.status, acp::ToolCallStatus::Completed |
acp::ToolCallStatus::Failed
tc.status,
acp::ToolCallStatus::Completed | acp::ToolCallStatus::Failed
);
if is_pre_completed {} else {
pending_tool_calls.insert(tc.tool_call_id.clone(), tc.clone());
@ -1445,13 +1454,11 @@ impl MvpAgent {
target_client_id: Option<&serde_json::Value>,
cursor: Option<&str>,
) -> Result<(u64, u64, Vec<(String, String)>), acp::Error> {
let mut replay_timer = crate::instrumentation_timer!(
"session.load_session_replay"
);
let mut replay_timer = crate::instrumentation_timer!("session.load_session_replay");
replay_timer.with_field("session_id", session_id.0.as_ref());
replay_timer.with_field("cwd", cwd.as_str());
let Some(updates_path) = updates_file_path.clone() else {
tracing::warn!(session_id = % session_id.0, "replay: no updates file path");
tracing::warn!(session_id = %session_id.0, "replay: no updates file path");
return Ok((0, 0, Vec::new()));
};
let file_size = std::fs::metadata(&updates_path).map(|m| m.len()).unwrap_or(0);
@ -1469,13 +1476,15 @@ impl MvpAgent {
let sending = prepared.lines.len();
if prepared.mark_replay {
tracing::warn!(
session_id = % session_id.0,
session_id = %session_id.0,
"replay: cursor not found, falling back to full replay"
);
} else {
tracing::info!(
session_id = % session_id.0, skipped = prepared.total_live - sending,
remaining = sending, "replay: cursor found, skipping events"
session_id = %session_id.0,
skipped = prepared.total_live - sending,
remaining = sending,
"replay: cursor found, skipping events"
);
}
}
@ -1510,15 +1519,16 @@ impl MvpAgent {
);
}
{
let _timer = crate::instrumentation_timer!(
"session.replay.drain_completions"
);
let _timer = crate::instrumentation_timer!("session.replay.drain_completions");
for rx in completions {
let _ = rx.await;
}
}
tracing::info!(
session_id = % session_id.0, updates_count, end_offset, file_size,
session_id = %session_id.0,
updates_count,
end_offset,
file_size,
"replay: completed"
);
replay_timer.with_field("updates_count", updates_count);
@ -1579,7 +1589,9 @@ impl MvpAgent {
}
if delta_count > 0 {
tracing::info!(
session_id = % session_id.0, delta_count, from_offset,
session_id = %session_id.0,
delta_count,
from_offset,
"Delta replay enqueued updates (drain pending)"
);
}
@ -1702,7 +1714,8 @@ impl MvpAgent {
}
if !completions.is_empty() {
tracing::info!(
session_id = % session_id.0, stale_count = completions.len(),
session_id = %session_id.0,
stale_count = completions.len(),
"Emitted task_completed for stale background tasks"
);
}
@ -1745,7 +1758,7 @@ impl MvpAgent {
.unwrap_or(0);
if result == 0 {
tracing::warn!(
path = % updates_path.display(),
path = %updates_path.display(),
"extract_initial_tokens: no totalTokens found in updates tail, \
token tracking will rely on conversation estimate until first model response"
);
@ -1805,15 +1818,17 @@ impl MvpAgent {
.await;
if let Some(unblocked) = result {
tracing::info!(
new_tier = % unblocked.new_tier, "subscription detected, lifting gate"
new_tier = %unblocked.new_tier,
"subscription detected, lifting gate"
);
xai_grok_telemetry::unified_log::info(
"paywall_check_gate_lifting",
None,
Some(
serde_json::json!(
{ "user_id" : user_id, "new_tier" : unblocked.new_tier, }
),
serde_json::json!({
"user_id": user_id,
"new_tier": unblocked.new_tier,
}),
),
);
if let Some(settings) = unblocked.settings {
@ -1836,16 +1851,17 @@ impl MvpAgent {
&& !settings_allow_access(self.cfg.borrow().remote_settings.as_ref())
{
tracing::info!(
new_tier = % unblocked.new_tier,
new_tier = %unblocked.new_tier,
"subscription detected but allow_access still false, keeping gate"
);
xai_grok_telemetry::unified_log::warn(
"paywall_check_gate_kept_allow_access_false",
None,
Some(
serde_json::json!(
{ "user_id" : user_id, "new_tier" : unblocked.new_tier, }
),
serde_json::json!({
"user_id": user_id,
"new_tier": unblocked.new_tier,
}),
),
);
return;
@ -1864,23 +1880,21 @@ impl MvpAgent {
xai_grok_telemetry::unified_log::info(
"paywall_check_jwt_refreshed",
None,
Some(serde_json::json!({ "user_id" : user_id })),
Some(serde_json::json!({ "user_id": user_id })),
);
true
}
Err(e) => {
tracing::warn!(
error = % e,
"post-unblock: JWT refresh failed, user may need to re-login on next restart"
);
tracing::warn!(error = %e, "post-unblock: JWT refresh failed, user may need to re-login on next restart");
xai_grok_telemetry::unified_log::warn(
"paywall_check_error",
None,
Some(
serde_json::json!(
{ "user_id" : user_id, "kind" :
"post_unblock_refresh_failed", "detail" : e.to_string(), }
),
serde_json::json!({
"user_id": user_id,
"kind": "post_unblock_refresh_failed",
"detail": e.to_string(),
}),
),
);
false
@ -1906,28 +1920,34 @@ impl MvpAgent {
"model catalog: post_subscription_unblock refresh",
None,
Some(
serde_json::json!(
{ "user_id" : user_id_log, "new_tier" : new_tier,
"refresh_ok" : refresh_ok, "jwt_claim" : jwt_claim_log,
"jwt_matches_new_tier" : true, }
),
serde_json::json!({
"user_id": user_id_log,
"new_tier": new_tier,
"refresh_ok": refresh_ok,
"jwt_claim": jwt_claim_log,
"jwt_matches_new_tier": true,
}),
),
);
models_manager.on_auth_changed().await;
});
} else {
tracing::warn!(
refresh_ok, jwt_claim = ? jwt_claim, new_tier = % unblocked.new_tier,
refresh_ok,
jwt_claim = ?jwt_claim,
new_tier = %unblocked.new_tier,
"post-unblock: JWT tier claim missing or stale vs live tier; deferring model catalog refresh with retry"
);
xai_grok_telemetry::unified_log::warn(
"model catalog: post_subscription_unblock deferred (jwt tier missing or stale)",
None,
Some(
serde_json::json!(
{ "user_id" : user_id, "new_tier" : unblocked.new_tier,
"refresh_ok" : refresh_ok, "jwt_claim" : jwt_claim, }
),
serde_json::json!({
"user_id": user_id,
"new_tier": unblocked.new_tier,
"refresh_ok": refresh_ok,
"jwt_claim": jwt_claim,
}),
),
);
spawn_post_unblock_jwt_and_catalog_retry(
@ -1942,7 +1962,9 @@ impl MvpAgent {
xai_grok_telemetry::unified_log::info(
"paywall_check_no_subscription",
None,
Some(serde_json::json!({ "user_id" : user_id, })),
Some(serde_json::json!({
"user_id": user_id,
})),
);
}
}
@ -2053,7 +2075,7 @@ impl MvpAgent {
if let Err(e) = xai_fast_worktree::WorktreeDb::open_default()
.and_then(|db| xai_fast_worktree::maybe_auto_gc(&db, &opts))
{
tracing::warn!(error = % e, "auto worktree gc failed");
tracing::warn!(error = %e, "auto worktree gc failed");
}
});
}
@ -2065,6 +2087,9 @@ impl MvpAgent {
SettingsUpdateNotification {
show_resolved_model: rs.and_then(|s| s.show_resolved_model),
sharing_enabled: rs.and_then(|s| s.sharing_enabled),
privacy_notice_rollout: rs.and_then(|s| s.privacy_notice_rollout),
privacy_banner_reshow_days: rs
.and_then(|s| s.privacy_banner_reshow_days),
session_picker_grouped: rs.and_then(|s| s.session_picker_grouped),
tips: rs.and_then(|s| s.tips.clone()),
announcements: rs.and_then(|s| s.announcements.clone()),
@ -2190,9 +2215,7 @@ impl MvpAgent {
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
tracing::debug!(
"proactive bundle sync skipped: another sync is already in flight"
);
tracing::debug!("proactive bundle sync skipped: another sync is already in flight");
return;
}
let proxy_base_url = self.cli_chat_proxy_base_url();
@ -2215,15 +2238,18 @@ impl MvpAgent {
match result {
Ok(Some(res)) => {
tracing::info!(
version = % res.version, personas = res.personas_count, roles =
res.roles_count, agents = res.agents_count, skills = res
.skills_count, "proactive bundle sync complete"
version = %res.version,
personas = res.personas_count,
roles = res.roles_count,
agents = res.agents_count,
skills = res.skills_count,
"proactive bundle sync complete"
);
Self::broadcast_refresh_skill_baseline(senders);
}
Ok(None) => {}
Err(err) => {
tracing::warn!(error = % err, "proactive bundle sync failed");
tracing::warn!(error = %err, "proactive bundle sync failed");
}
}
});
@ -2247,7 +2273,8 @@ async fn handle_synthetic_turn_trace(
};
let Some(info) = session_info else {
tracing::debug!(
session_id = % request.session_id.0, prompt_id = % request.prompt_id,
session_id = %request.session_id.0,
prompt_id = %request.prompt_id,
"Synthetic trace: session not found, skipping",
);
return;
@ -2283,7 +2310,8 @@ async fn handle_synthetic_turn_trace(
let trace_context = this.get_trace_context(&info, turn_number).await;
let Some(ctx) = trace_context else {
tracing::info!(
session_id = % request.session_id.0, prompt_id = % request.prompt_id,
session_id = %request.session_id.0,
prompt_id = %request.prompt_id,
"Synthetic trace: trace uploads disabled, skipping",
);
return;
@ -2323,16 +2351,21 @@ async fn handle_synthetic_turn_trace(
"synthetic_before_uploads",
async move {
futures::join!(
upload_session_state(& before_ctx, "before", request
.before_session_copy_rx, UploadWait::Confirm,), upload_metadata(&
before_ctx, metadata),
);
upload_session_state(
&before_ctx,
"before",
request.before_session_copy_rx,
UploadWait::Confirm,
),
upload_metadata(&before_ctx, metadata),
);
},
);
let turn_result = request.completion_rx.await;
let Ok(prompt_result) = turn_result else {
tracing::debug!(
session_id = % request.session_id.0, prompt_id = % request.prompt_id,
session_id = %request.session_id.0,
prompt_id = %request.prompt_id,
"Synthetic trace: turn completion channel dropped, skipping",
);
return;
@ -2417,9 +2450,7 @@ async fn handle_synthetic_turn_trace(
.send(SessionCommand::CopyFile {
respond_to: session_copy_tx,
});
let synthetic_committed = matches!(
& prompt_result, Ok(ok) if matches!(ok.stop_reason, acp::StopReason::EndTurn)
);
let synthetic_committed = matches!(&prompt_result, Ok(ok) if matches!(ok.stop_reason, acp::StopReason::EndTurn));
let streaming_partial = crate::upload::turn::take_streaming_partial(
&ctx.session_handle.cmd_tx,
request.prompt_id.clone(),
@ -2464,8 +2495,9 @@ async fn handle_synthetic_turn_trace(
Ok(_) => {}
Err(e) => {
tracing::warn!(
error = % e, "Synthetic turn trace upload failed (non-fatal)",
);
error = %e,
"Synthetic turn trace upload failed (non-fatal)",
);
}
}
},
@ -2513,7 +2545,10 @@ fn spawn_post_unblock_jwt_and_catalog_retry(
xai_grok_telemetry::unified_log::info(
"model catalog: post_subscription_unblock jwt retry skipped (already in flight)",
None,
Some(serde_json::json!({ "user_id" : user_id, "new_tier" : new_tier, })),
Some(serde_json::json!({
"user_id": user_id,
"new_tier": new_tier,
})),
);
return;
}
@ -2549,14 +2584,10 @@ fn spawn_post_unblock_jwt_and_catalog_retry(
let detail = match (&refresh_result, &jwt_claim) {
(Ok(_), None) => "refresh_ok but no tier claim".to_string(),
(Ok(_), Some(c)) => {
format!(
"refresh_ok but stale tier claim={c} (want {new_tier})"
)
format!("refresh_ok but stale tier claim={c} (want {new_tier})")
}
(Err(e), Some(c)) => {
format!(
"refresh_err={e}; stale tier claim={c} (want {new_tier})"
)
format!("refresh_err={e}; stale tier claim={c} (want {new_tier})")
}
(Err(e), None) => e.to_string(),
};
@ -2572,11 +2603,13 @@ fn spawn_post_unblock_jwt_and_catalog_retry(
"model catalog: post_subscription_unblock jwt retry scheduled",
None,
Some(
serde_json::json!(
{ "user_id" : user_id, "new_tier" : new_tier, "attempt" :
attempt, "max_retries" : max_retries, "delay_ms" : delay
.as_millis() as u64, }
),
serde_json::json!({
"user_id": user_id,
"new_tier": new_tier,
"attempt": attempt,
"max_retries": max_retries,
"delay_ms": delay.as_millis() as u64,
}),
),
);
}
@ -2589,9 +2622,10 @@ fn spawn_post_unblock_jwt_and_catalog_retry(
"model catalog: post_subscription_unblock refresh (after jwt retry)",
None,
Some(
serde_json::json!(
{ "user_id" : user_id, "new_tier" : new_tier, }
),
serde_json::json!({
"user_id": user_id,
"new_tier": new_tier,
}),
),
);
models_manager.on_auth_changed().await;
@ -2601,10 +2635,11 @@ fn spawn_post_unblock_jwt_and_catalog_retry(
"model catalog: post_subscription_unblock jwt retry exhausted",
None,
Some(
serde_json::json!(
{ "user_id" : user_id, "new_tier" : new_tier, "error" : e
.to_string(), }
),
serde_json::json!({
"user_id": user_id,
"new_tier": new_tier,
"error": e.to_string(),
}),
),
);
}

View file

@ -10,6 +10,7 @@ fn args<'a>(
) -> PromptResponseMetaArgs<'a> {
PromptResponseMetaArgs {
session_id,
tool_overrides: None,
prompt_id,
total_tokens,
model_id,
@ -119,6 +120,31 @@ fn cancel_trigger_lands_as_camelcase_meta_key() {
assert!(none.get("cancelTrigger").is_none());
}
#[test]
fn tool_overrides_land_as_camelcase_meta_key() {
let overrides = xai_grok_sampling_types::ToolOverrides {
x_search: Some(xai_grok_sampling_types::XSearchOptions {
date_bound: Some(
xai_grok_sampling_types::SearchDateBound::new(None, Some("2024-03-15".to_string()))
.unwrap(),
),
}),
web_search: None,
};
let meta = build_prompt_response_meta(PromptResponseMetaArgs {
tool_overrides: Some(overrides),
..args("s", "p", 0, "m")
});
assert_eq!(
meta["toolOverrides"]["xSearch"]["dateBound"]["toDate"],
"2024-03-15"
);
assert!(meta["toolOverrides"].get("webSearch").is_none());
let none = build_prompt_response_meta(args("s", "p", 0, "m"));
assert!(none.get("toolOverrides").is_none());
}
#[test]
fn structured_output_maps_to_camelcase_meta_keys() {
// Success carries the validated value under `structuredOutput`; no error key.

View file

@ -23,9 +23,7 @@ impl MvpAgent {
let sid = id.0.to_string();
tokio::spawn(async move {
if let Err(e) = client.finalize(&sid).await {
tracing::warn!(
error = % e, "session registry finalize failed (non-fatal)"
);
tracing::warn!(error = %e, "session registry finalize failed (non-fatal)");
}
});
}
@ -46,6 +44,9 @@ impl MvpAgent {
if let Some(ops) = self.workspace_ops.borrow().as_ref() {
ops.end_local_session(id.0.as_ref());
}
self.subagent_coordinator
.borrow_mut()
.discard_pending_completions_for(id.0.as_ref());
}
/// Get-or-create the per-session dispatch lock (see
/// [`Self::dispatch_locks`]). Cheap clone of the shared `Rc`.
@ -85,7 +86,9 @@ impl MvpAgent {
.borrow_mut()
.push((id.0.to_string(), final_state));
tracing::debug!(
session_id = % id.0, ? final_state, "roster delta: session removed"
session_id = %id.0,
?final_state,
"roster delta: session removed"
);
self.emit_roster_changed(Vec::new(), vec![id.0.to_string()]);
}
@ -298,7 +301,7 @@ impl MvpAgent {
for id in dead {
if self.sessions.borrow().contains_key(&id) {
tracing::warn!(
session_id = % id.0,
session_id = %id.0,
"Resident session actor exited unexpectedly; reaping as DeadFailed"
);
self.reap_dead_session(&id);
@ -306,7 +309,7 @@ impl MvpAgent {
self.session_threads.borrow_mut().remove(&id);
self.session_live_state.borrow_mut().remove(&id);
tracing::debug!(
session_id = % id.0,
session_id = %id.0,
"Reaped finished thread for non-resident session (clean exit)"
);
}

View file

@ -41,8 +41,9 @@ impl MvpAgent {
};
if let Some((root, inherited_loop)) = reparent {
tracing::info!(
child_session_id = % child_sess, root_session_id = % root,
subagent_id = % request.id,
child_session_id = %child_sess,
root_session_id = %root,
subagent_id = %request.id,
"Re-parenting child-session spawn to root session"
);
request.parent_session_id = root;
@ -68,8 +69,8 @@ impl MvpAgent {
this.try_build_subagent_spawn_context(&parent_sid)
else {
tracing::warn!(
parent_session_id = % parent_sid, subagent_id = % request
.id,
parent_session_id = %parent_sid,
subagent_id = %request.id,
"Spawn for unknown/evicted parent session, failing request"
);
this.subagent_coordinator
@ -248,7 +249,7 @@ impl MvpAgent {
let mut completions = this
.subagent_coordinator
.borrow_mut()
.drain_pending_completions();
.drain_pending_completions_for(&request.session_id);
completions.retain(|c| !request.suppress_ids.contains(&c.subagent_id));
let _ = request.respond_to.send(completions);
}
@ -301,8 +302,8 @@ impl MvpAgent {
),
None => {
tracing::warn!(
parent_session_id = % request.parent_session_id,
subagent_type = % request.subagent_type,
parent_session_id = %request.parent_session_id,
subagent_type = %request.subagent_type,
"DescribeType for unknown/evicted parent session, replying Unavailable",
);
SubagentDescribeOutcome::Unavailable
@ -543,6 +544,12 @@ impl MvpAgent {
&parent_cwd,
project_trusted,
);
let inherited_tool_overrides = {
let sessions = self.sessions.borrow();
sessions
.get(&parent_sid)
.and_then(|ps| ps.resolved_tool_overrides.load_full().map(|o| (*o).clone()))
};
Some(crate::agent::subagent::SubagentSpawnContext {
lsp: parent_lsp,
gateway: self.gateway.clone(),
@ -562,6 +569,7 @@ impl MvpAgent {
auth: self.current_or_buffered_auth(),
parent_cwd: parent_cwd.clone(),
parent_session_id: parent_session_id.to_string(),
inherited_tool_overrides,
yolo_mode,
subagent_event_tx: self.subagent_event_tx.clone(),
parent_depth,

View file

@ -760,10 +760,10 @@ fn resolve_agent_definition_acp_profile_wins_for_explicit_grok_build_family() {
std::env::remove_var("GROK_AGENT");
}
let tmp = tempfile::tempdir().unwrap();
let acp_profile = xai_grok_agent::AgentDefinition::from_json(&serde_json::json!(
{ "name" : "custom-devbox-profile", "description" :
"Custom devbox profile", }
))
let acp_profile = xai_grok_agent::AgentDefinition::from_json(&serde_json::json!({
"name": "custom-devbox-profile",
"description": "Custom devbox profile",
}))
.expect("agent definition must parse");
for family_variant in ["grok-build", "grok-build-plan", "grok-build-concise"] {
let def = MvpAgent::resolve_agent_definition(
@ -869,8 +869,8 @@ fn resolve_agent_definition_agent_profile_with_model_override() {
}
#[test]
fn read_session_or_init_meta_str_prefers_session_meta() {
let session = serde_json::json!({ "rules" : "from-session" });
let init = serde_json::json!({ "rules" : "from-init" });
let session = serde_json::json!({ "rules": "from-session" });
let init = serde_json::json!({ "rules": "from-init" });
assert_eq!(
read_session_or_init_meta_str(session.as_object(), init.as_object(), "rules"),
Some("from-session"),
@ -878,8 +878,8 @@ fn read_session_or_init_meta_str_prefers_session_meta() {
}
#[test]
fn read_session_or_init_meta_str_falls_back_to_init_meta() {
let session = serde_json::json!({ "other" : "x" });
let init = serde_json::json!({ "rules" : "from-init" });
let session = serde_json::json!({ "other": "x" });
let init = serde_json::json!({ "rules": "from-init" });
assert_eq!(
read_session_or_init_meta_str(session.as_object(), init.as_object(), "rules"),
Some("from-init"),
@ -896,10 +896,15 @@ fn parse_session_plugin_dirs_filters_and_dedupes() {
std::fs::create_dir(&dir).unwrap();
let file = tmp.path().join("file.txt");
std::fs::write(&file, "x").unwrap();
let meta = serde_json::json!(
{ "pluginDirs" : [dir.to_string_lossy(), dir.to_string_lossy(), file
.to_string_lossy(), "relative/path", 42,] }
);
let meta = serde_json::json!({
"pluginDirs": [
dir.to_string_lossy(), // kept
dir.to_string_lossy(), // duplicate → deduped
file.to_string_lossy(), // not a directory → skipped
"relative/path", // not absolute → skipped
42, // not a string → skipped
]
});
assert_eq!(parse_session_plugin_dirs(meta.as_object()), vec![dir]);
assert!(parse_session_plugin_dirs(None).is_empty());
assert!(parse_session_plugin_dirs(serde_json::json!({}).as_object()).is_empty());
@ -907,7 +912,7 @@ fn parse_session_plugin_dirs_filters_and_dedupes() {
#[test]
fn read_session_or_init_meta_str_returns_none_when_absent() {
assert_eq!(read_session_or_init_meta_str(None, None, "rules"), None,);
let session = serde_json::json!({ "other" : "x" });
let session = serde_json::json!({ "other": "x" });
assert_eq!(
read_session_or_init_meta_str(session.as_object(), None, "rules"),
None,
@ -915,8 +920,8 @@ fn read_session_or_init_meta_str_returns_none_when_absent() {
}
#[test]
fn read_session_or_init_meta_str_ignores_non_string_values() {
let session = serde_json::json!({ "rules" : 42 });
let init = serde_json::json!({ "rules" : "from-init" });
let session = serde_json::json!({ "rules": 42 });
let init = serde_json::json!({ "rules": "from-init" });
assert_eq!(
read_session_or_init_meta_str(session.as_object(), init.as_object(), "rules"),
Some("from-init"),
@ -924,8 +929,8 @@ fn read_session_or_init_meta_str_ignores_non_string_values() {
}
#[test]
fn system_prompt_override_from_meta_prefers_session_and_rejects_empty() {
let session = serde_json::json!({ "systemPromptOverride" : "from session" });
let init = serde_json::json!({ "systemPromptOverride" : "from init" });
let session = serde_json::json!({ "systemPromptOverride": "from session" });
let init = serde_json::json!({ "systemPromptOverride": "from init" });
assert_eq!(
system_prompt_override_from_meta(session.as_object(), init.as_object()),
Some("from session")
@ -934,7 +939,7 @@ fn system_prompt_override_from_meta_prefers_session_and_rejects_empty() {
system_prompt_override_from_meta(None, init.as_object()),
Some("from init")
);
let empty = serde_json::json!({ "systemPromptOverride" : "" });
let empty = serde_json::json!({ "systemPromptOverride": "" });
assert_eq!(
system_prompt_override_from_meta(empty.as_object(), None),
None
@ -945,8 +950,8 @@ fn system_prompt_override_from_meta_prefers_session_and_rejects_empty() {
fn enqueue_replace_system_prompt_override_sends_when_present() {
use crate::session::SessionCommand;
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let session = serde_json::json!({ "systemPromptOverride" : "from session" });
let init = serde_json::json!({ "systemPromptOverride" : "from init" });
let session = serde_json::json!({ "systemPromptOverride": "from session" });
let init = serde_json::json!({ "systemPromptOverride": "from init" });
enqueue_replace_system_prompt_override(&tx, session.as_object(), init.as_object());
match rx.try_recv() {
Ok(SessionCommand::ReplaceSystemPrompt { system_prompt }) => {
@ -960,7 +965,7 @@ fn enqueue_replace_system_prompt_override_noop_when_absent_or_empty() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
enqueue_replace_system_prompt_override(
&tx,
serde_json::json!({ "systemPromptOverride" : "" }).as_object(),
serde_json::json!({ "systemPromptOverride": "" }).as_object(),
None,
);
enqueue_replace_system_prompt_override(&tx, serde_json::json!({}).as_object(), None);
@ -1140,6 +1145,7 @@ fn make_test_handle(
cwd: "/tmp".to_string(),
},
max_turns: None,
resolved_tool_overrides: std::sync::Arc::new(arc_swap::ArcSwapOption::empty()),
hunk_tracker_handle,
chat_state_handle: xai_chat_state::ChatStateHandle::noop(),
signals_handle: crate::session::signals::SessionSignalsHandle::new(),
@ -1465,7 +1471,7 @@ fn parse_code_nav_capability_present_and_true() {
let mut meta = serde_json::Map::new();
meta.insert(
"x.ai/codeNavigation".to_string(),
serde_json::json!({ "enabled" : true }),
serde_json::json!({ "enabled": true }),
);
let init = acp::InitializeRequest::new(acp::ProtocolVersion::V1).client_capabilities(
acp::ClientCapabilities::new()
@ -1489,7 +1495,7 @@ fn parse_code_nav_capability_false_returns_false() {
let mut meta = serde_json::Map::new();
meta.insert(
"x.ai/codeNavigation".to_string(),
serde_json::json!({ "enabled" : false }),
serde_json::json!({ "enabled": false }),
);
let init = acp::InitializeRequest::new(acp::ProtocolVersion::V1).client_capabilities(
acp::ClientCapabilities::new()
@ -1614,7 +1620,7 @@ fn build_minimal_agent_for_tests() -> MvpAgent {
fn session_usage_request(session_id: &str) -> acp::ExtRequest {
acp::ExtRequest::new(
"x.ai/session/usage",
serde_json::value::to_raw_value(&serde_json::json!({ "sessionId" : session_id }))
serde_json::value::to_raw_value(&serde_json::json!({ "sessionId": session_id }))
.unwrap()
.into(),
)
@ -2285,10 +2291,10 @@ fn orphaned_tasks_filters_rewind_dead_branches() {
}
#[test]
fn allow_access_from_remote_settings() {
let json = serde_json::json!({ "allow_access" : true });
let json = serde_json::json!({ "allow_access": true });
let rs: crate::util::config::RemoteSettings = serde_json::from_value(json).unwrap();
assert_eq!(rs.allow_access, Some(true));
let json = serde_json::json!({ "allow_access" : false });
let json = serde_json::json!({ "allow_access": false });
let rs: crate::util::config::RemoteSettings = serde_json::from_value(json).unwrap();
assert_eq!(rs.allow_access, Some(false));
let json = serde_json::json!({});
@ -2297,7 +2303,7 @@ fn allow_access_from_remote_settings() {
}
#[test]
fn on_demand_enabled_from_remote_settings() {
let json = serde_json::json!({ "on_demand_enabled" : false });
let json = serde_json::json!({ "on_demand_enabled": false });
let rs: crate::util::config::RemoteSettings = serde_json::from_value(json).unwrap();
assert_eq!(rs.on_demand_enabled, Some(false));
let json = serde_json::json!({});
@ -2530,6 +2536,21 @@ async fn prepare_video_gen_config_disabled_when_zdr_flag_set() {
};
assert!(zdr_video_output_s3.as_ref().is_some_and(|c| c.is_valid()));
}
#[tokio::test(flavor = "current_thread")]
async fn prepare_video_gen_config_respects_feature_flag() {
use xai_grok_tools::implementations::grok_build::video_gen::VideoGenConfig;
let agent = build_minimal_agent_for_tests();
agent.sampling_config.borrow_mut().api_key = Some("test-key".to_string());
assert!(matches!(
agent.prepare_video_gen_config(),
VideoGenConfig::Enabled { .. }
));
agent.cfg.borrow_mut().features.video_gen = Some(false);
assert!(matches!(
agent.prepare_video_gen_config(),
VideoGenConfig::Disabled
));
}
/// The imagine tier gate fails **open**: with no resolved auth we can't confirm
/// a restricted personal tier, so the tools stay advertised and un-flagged (the
/// server 429 remains the authoritative backstop). Guards against accidentally
@ -2846,22 +2867,22 @@ fn parse_session_kind_matrix() {
let cases: &[(&str, serde_json::Value, SessionKind)] = &[
(
"chat",
json!({ "x.ai/session" : { "kind" : "chat" } }),
json!({"x.ai/session": {"kind": "chat"}}),
SessionKind::Chat,
),
(
"build",
json!({ "x.ai/session" : { "kind" : "build" } }),
json!({"x.ai/session": {"kind": "build"}}),
SessionKind::Build,
),
(
"chat_malformed_sibling",
json!({ "x.ai/session" : { "kind" : "chat", "facets" : "not-a-map" } }),
json!({"x.ai/session": {"kind": "chat", "facets": "not-a-map"}}),
SessionKind::Chat,
),
(
"unknown_kind",
json!({ "x.ai/session" : { "kind" : "frob" } }),
json!({"x.ai/session": {"kind": "frob"}}),
SessionKind::Build,
),
("absent", json!({}), SessionKind::Build),
@ -3036,7 +3057,7 @@ fn ext_method_rewind_uses_local_dispatch_without_bridge() {
let _env = crate::env::EnvVarGuard::remove(crate::env::GROK_DISABLE_CUSTOM_BRIDGE_ENV);
run_local_for_bridge_test(|| async {
let agent = build_minimal_agent_for_tests();
let params = serde_json::json!({ "sessionId" : "sess-local" });
let params = serde_json::json!({ "sessionId": "sess-local" });
let err = agent
.ext_method(acp::ExtRequest::new(
"x.ai/rewind/points",
@ -3184,7 +3205,7 @@ async fn drive_disconnect(agent: &MvpAgent, sid: &acp::SessionId) {
async fn drive_disconnect_many(agent: &MvpAgent, sids: &[&acp::SessionId]) {
use acp::Agent as _;
let ids: Vec<&str> = sids.iter().map(|s| s.0.as_ref()).collect();
let params = serde_json::json!({ "sessionIds" : ids });
let params = serde_json::json!({ "sessionIds": ids });
let raw = serde_json::value::to_raw_value(&params).unwrap();
agent
.ext_notification(acp::ExtNotification::new(
@ -3199,7 +3220,7 @@ async fn drive_disconnect_many(agent: &MvpAgent, sids: &[&acp::SessionId]) {
/// exercising the exact production path that finalizes the replica.
async fn drive_close(agent: &MvpAgent, session_id: &str) -> Result<acp::ExtResponse, acp::Error> {
use acp::Agent as _;
let params = serde_json::json!({ "sessionId" : session_id });
let params = serde_json::json!({ "sessionId": session_id });
let raw = serde_json::value::to_raw_value(&params).unwrap();
agent
.ext_method(acp::ExtRequest::new(
@ -3872,7 +3893,7 @@ async fn answer_folder_trust_request(
assert_eq!(args.request.method.as_ref(), "x.ai/folder_trust/request");
let params: serde_json::Value = serde_json::from_str(args.request.params.get()).unwrap();
let resp: acp::ExtResponse = acp::ExtResponse::new(std::sync::Arc::from(
serde_json::value::to_raw_value(&serde_json::json!({ "outcome" : outcome })).unwrap(),
serde_json::value::to_raw_value(&serde_json::json!({ "outcome": outcome })).unwrap(),
));
let _ = args.response_tx.send(Ok(resp));
params
@ -4652,23 +4673,26 @@ mod direct_hub_cloud_removed {
}
#[test]
fn cloud_server_id_meta_is_hard_error() {
let meta = serde_json::json!({ "x.ai/cloud_server_id" : "srv-123" });
let meta = serde_json::json!({ "x.ai/cloud_server_id": "srv-123" });
let err = reject_direct_hub_cloud_meta(meta.as_object()).expect_err("must reject");
assert_direct_hub_error(err);
}
#[test]
fn cloud_server_id_null_still_present_is_hard_error() {
let meta = serde_json::json!({ "x.ai/cloud_server_id" : null });
let meta = serde_json::json!({ "x.ai/cloud_server_id": null });
let err = reject_direct_hub_cloud_meta(meta.as_object()).expect_err("must reject");
assert_direct_hub_error(err);
}
#[test]
fn cloud_server_id_with_gateway_meta_still_hard_error() {
let meta = serde_json::json!(
{ "x.ai/cloud_server_id" : "srv-legacy", "envId" : "env-1",
"x.ai/cloud_existing_workspace" : { "server_id" : "ws-1", "cwd" :
"/workspace" } }
);
let meta = serde_json::json!({
"x.ai/cloud_server_id": "srv-legacy",
"envId": "env-1",
"x.ai/cloud_existing_workspace": {
"server_id": "ws-1",
"cwd": "/workspace"
}
});
let err = reject_direct_hub_cloud_meta(meta.as_object()).expect_err("Direct stamp wins");
assert_direct_hub_error(err);
}
@ -4677,14 +4701,22 @@ mod direct_hub_cloud_removed {
assert!(reject_direct_hub_cloud_meta(None).is_ok());
assert!(reject_direct_hub_cloud_meta(serde_json::json!({}).as_object()).is_ok());
assert!(
reject_direct_hub_cloud_meta(serde_json::json!({ "envId" : "env-1" }).as_object())
.is_ok()
reject_direct_hub_cloud_meta(
serde_json::json!({
"envId": "env-1"
})
.as_object()
)
.is_ok()
);
assert!(
reject_direct_hub_cloud_meta(
serde_json::json!({
"x.ai/cloud_existing_workspace" : { "server_id" : "ws-1", "cwd" :
"/workspace" } })
"x.ai/cloud_existing_workspace": {
"server_id": "ws-1",
"cwd": "/workspace"
}
})
.as_object()
)
.is_ok()
@ -4715,10 +4747,11 @@ mod direct_hub_cloud_removed {
vec!["url"],
"HubConfig must only serialize url (no proxy-mode fields)"
);
let from_legacy: HubConfig = serde_json::from_value(serde_json::json!(
{ "url" : "wss://hub.example/ws", "workspace_mode" : "remote",
"send_turn_hooks" : false, }
))
let from_legacy: HubConfig = serde_json::from_value(serde_json::json!({
"url": "wss://hub.example/ws",
"workspace_mode": "remote",
"send_turn_hooks": false,
}))
.expect("ignore unknown fields");
assert_eq!(from_legacy.url.as_deref(), Some("wss://hub.example/ws"));
}

View file

@ -1,6 +1,6 @@
//! Subagent spawn-context inheritance: a child session must inherit the parent's
//! permission handle and goal-loop gate so policy and run-state can't be bypassed
//! by delegating to a subagent.
//! permission handle, goal-loop gate, and configured tool-overrides cutoff so policy,
//! run-state, and a backtest bound can't be bypassed by delegating to a subagent.
use super::{build_minimal_agent_for_tests, make_test_handle};
use agent_client_protocol as acp;
@ -133,3 +133,43 @@ async fn subagent_spawn_context_inherits_parent_ask_user_question_gate() {
"subagent must inherit the parent's enabled ask_user_question gate"
);
}
#[tokio::test]
async fn subagent_spawn_context_inherits_parent_configured_cutoff() {
let agent = build_minimal_agent_for_tests();
let cutoff = xai_grok_sampling_types::ToolOverrides {
x_search: Some(xai_grok_sampling_types::XSearchOptions {
date_bound: Some(
xai_grok_sampling_types::SearchDateBound::new(None, Some("2020-01-01".to_string()))
.unwrap(),
),
}),
web_search: None,
};
let sid = acp::SessionId::new("parent-cutoff");
let handle = make_test_handle("test-model", false, None);
handle
.resolved_tool_overrides
.store(Some(std::sync::Arc::new(cutoff.clone())));
agent.sessions.borrow_mut().insert(sid.clone(), handle);
let ctx = agent.build_subagent_spawn_context(sid.0.as_ref());
assert_eq!(
ctx.inherited_tool_overrides,
Some(cutoff),
"subagent context must inherit the parent's configured cutoff for its first-turn update"
);
// A parent with no configured cutoff must not fabricate one for the child.
let sid_none = acp::SessionId::new("parent-unbounded");
agent.sessions.borrow_mut().insert(
sid_none.clone(),
make_test_handle("test-model", false, None),
);
let ctx_none = agent.build_subagent_spawn_context(sid_none.0.as_ref());
assert!(
ctx_none.inherited_tool_overrides.is_none(),
"an unbounded parent must not hand a subagent a cutoff"
);
}

View file

@ -157,10 +157,7 @@ fn is_handshake_unauthorized(err: &anyhow::Error) -> bool {
use tokio_tungstenite::tungstenite::Error as WsError;
err.downcast_ref::<WsError>()
.map(|ws_err| {
matches!(
ws_err, WsError::Http(resp) if resp.status() ==
reqwest::StatusCode::UNAUTHORIZED
)
matches!(ws_err, WsError::Http(resp) if resp.status() == reqwest::StatusCode::UNAUTHORIZED)
})
.unwrap_or(false)
}
@ -196,10 +193,10 @@ async fn attempt_auth_recovery(
xai_grok_telemetry::unified_log::warn(
"auth recovery: relay refresh timed out",
None,
Some(serde_json::json!(
{ "context" : context, "timeout_secs" :
AUTH_RECOVERY_TIMEOUT_SECS, }
)),
Some(serde_json::json!({
"context": context,
"timeout_secs": AUTH_RECOVERY_TIMEOUT_SECS,
})),
);
return false;
}
@ -210,10 +207,10 @@ async fn attempt_auth_recovery(
xai_grok_telemetry::unified_log::info(
"auth recovery: relay token unchanged, backing off",
None,
Some(serde_json::json!(
{ "context" : context, "key_prefix" : crate
::auth::token_suffix(& new_auth.key), }
)),
Some(serde_json::json!({
"context": context,
"key_prefix": crate::auth::token_suffix(&new_auth.key),
})),
);
false
}
@ -222,10 +219,10 @@ async fn attempt_auth_recovery(
xai_grok_telemetry::unified_log::info(
"auth recovery: relay recovered",
None,
Some(serde_json::json!(
{ "context" : context, "new_key_prefix" : crate
::auth::token_suffix(& new_auth.key), }
)),
Some(serde_json::json!({
"context": context,
"new_key_prefix": crate::auth::token_suffix(&new_auth.key),
})),
);
config.auth = new_auth;
true
@ -235,17 +232,17 @@ async fn attempt_auth_recovery(
xai_grok_telemetry::unified_log::warn(
"auth recovery: relay giving up (terminal)",
None,
Some(serde_json::json!({ "context" : context, "error" : format!("{e}") })),
Some(serde_json::json!({ "context": context, "error": format!("{e}") })),
);
cancel.cancel();
false
}
Err(e) => {
warn!(error = % e, "auth recovery: relay {context}, refresh failed");
warn!(error = %e, "auth recovery: relay {context}, refresh failed");
xai_grok_telemetry::unified_log::debug(
"auth recovery: relay refresh failed",
None,
Some(serde_json::json!({ "context" : context, "error" : format!("{e}") })),
Some(serde_json::json!({ "context": context, "error": format!("{e}") })),
);
false
}
@ -270,7 +267,8 @@ async fn run_relay_loop(
.and_then(proxy::resolve_proxy_for_host);
if let Some(ref url) = proxy_url {
info!(
proxy = % url, target = target_host.as_deref().unwrap_or("unknown"),
proxy = %url,
target = target_host.as_deref().unwrap_or("unknown"),
"Using HTTP CONNECT proxy for relay connections"
);
}
@ -280,14 +278,17 @@ async fn run_relay_loop(
break;
}
tracing::info!(
target : crate ::instrumentation::TARGET, event = "relay_connecting", ws_url
= % config.ws_url, attempt = reconnect_attempts,
target: crate::instrumentation::TARGET,
event = "relay_connecting",
ws_url = %config.ws_url,
attempt = reconnect_attempts,
);
match connect_to_relay(&config, proxy_url.as_deref(), &cancel).await {
Ok(ws) => {
tracing::info!(
target : crate ::instrumentation::TARGET, event = "relay_connected",
ws_url = % config.ws_url,
target: crate::instrumentation::TARGET,
event = "relay_connected",
ws_url = %config.ws_url,
);
reconnect_attempts = 0;
delay_secs = BASE_DELAY_SECS;
@ -309,15 +310,16 @@ async fn run_relay_loop(
}
}
Err(e) => {
warn!(error = ? e, "WebSocket session ended with error");
warn!(error = ?e, "WebSocket session ended with error");
}
}
if cancel.is_cancelled() {
break;
}
tracing::info!(
target : crate ::instrumentation::TARGET, event =
"relay_disconnected", ws_url = % config.ws_url,
target: crate::instrumentation::TARGET,
event = "relay_disconnected",
ws_url = %config.ws_url,
);
tprintln!("Disconnected from Grok WebSocket server");
info!("WebSocket disconnected, will reconnect");
@ -325,8 +327,10 @@ async fn run_relay_loop(
Err(e) => {
let handshake_401 = is_handshake_unauthorized(&e);
tracing::info!(
target : crate ::instrumentation::TARGET, event =
"relay_connection_failed", ws_url = % config.ws_url, error = % e,
target: crate::instrumentation::TARGET,
event = "relay_connection_failed",
ws_url = %config.ws_url,
error = %e,
handshake_401,
);
if handshake_401 {
@ -334,7 +338,7 @@ async fn run_relay_loop(
continue;
}
} else {
warn!(error = % e, "Failed to connect to WebSocket server");
warn!(error = %e, "Failed to connect to WebSocket server");
}
}
}
@ -350,8 +354,8 @@ async fn run_relay_loop(
reconnect_attempts
);
tokio::select! {
_ = cancel.cancelled() => break, _ =
tokio::time::sleep(Duration::from_secs(delay_secs)) => {}
_ = cancel.cancelled() => break,
_ = tokio::time::sleep(Duration::from_secs(delay_secs)) => {}
}
}
}
@ -406,21 +410,43 @@ async fn connect_to_relay(
let req = build_relay_request(config)?;
let connect_timeout = Duration::from_secs(CONNECT_TIMEOUT_SECS);
tokio::select! {
_ = cancel.cancelled() => { anyhow::bail!("Connection cancelled"); } result =
tokio::time::timeout(connect_timeout, async { if let Some(proxy_url) = proxy_url
{ let target_host = req.uri().host().ok_or_else(||
anyhow::anyhow!("WebSocket URL has no host")) ?; let target_port = req.uri()
.port_u16().unwrap_or(443); let tunneled_stream =
proxy::connect_via_proxy(proxy_url, target_host, target_port,). await ?; let (ws,
resp) = tokio_tungstenite::client_async(req, tunneled_stream). await .map_err(| e
| anyhow::Error::from(e).context("WebSocket handshake via proxy failed")) ?;
Ok((ws, resp)) } else { connect_async(req). await .map_err(| e |
anyhow::Error::from(e).context("WebSocket connection failed")) } }) => { match
result { Ok(Ok((ws, resp))) => { if let Some(proto) = resp.headers()
.get("Sec-WebSocket-Protocol") { info!(subprotocol = ? proto,
"WS subprotocol negotiated"); } Ok(ws) } Ok(Err(e)) => Err(e), Err(_) =>
anyhow::bail!("WebSocket connection timed out after {} seconds",
CONNECT_TIMEOUT_SECS), } }
_ = cancel.cancelled() => {
anyhow::bail!("Connection cancelled");
}
result = tokio::time::timeout(connect_timeout, async {
if let Some(proxy_url) = proxy_url {
// Proxy path: open TCP to proxy, send CONNECT, then WS handshake.
let target_host = req.uri().host()
.ok_or_else(|| anyhow::anyhow!("WebSocket URL has no host"))?;
let target_port = req.uri().port_u16().unwrap_or(443);
let tunneled_stream = proxy::connect_via_proxy(
proxy_url,
target_host,
target_port,
).await?;
// Perform the WebSocket handshake over the tunneled stream.
let (ws, resp) = tokio_tungstenite::client_async(req, tunneled_stream)
.await
.map_err(|e| anyhow::Error::from(e).context("WebSocket handshake via proxy failed"))?;
Ok((ws, resp))
} else {
// Direct path: no proxy needed.
connect_async(req)
.await
.map_err(|e| anyhow::Error::from(e).context("WebSocket connection failed"))
}
}) => {
match result {
Ok(Ok((ws, resp))) => {
if let Some(proto) = resp.headers().get("Sec-WebSocket-Protocol") {
info!(subprotocol = ?proto, "WS subprotocol negotiated");
}
Ok(ws)
}
Ok(Err(e)) => Err(e),
Err(_) => anyhow::bail!("WebSocket connection timed out after {} seconds", CONNECT_TIMEOUT_SECS),
}
}
}
}
/// Run a single WebSocket session, handling messages until disconnection.
@ -460,46 +486,109 @@ where
let read_from_ws = async move {
loop {
tokio::select! {
_ = cancel_read.cancelled() => break, msg_res =
tokio::time::timeout(liveness, ws_inbound.next()) => { let Ok(msg_opt) =
msg_res else { tprintln!("ws_inbound::liveness_timeout");
warn!(timeout_secs = liveness.as_secs(),
"no WS traffic within liveness window, treating connection as dead");
xai_grok_telemetry::unified_log::warn("relay: read liveness timeout, reconnecting",
None, Some(serde_json::json!({ "timeout_secs" : liveness.as_secs(),
})),); break; }; let Some(msg) = msg_opt else { break }; match msg {
Ok(Message::Text(text)) => { let trimmed_end = text
.trim_end_matches(['\r', '\n']); if trimmed_end.is_empty() {
debug!("received empty/whitespace WS text frame - skipping"); continue; }
let json : serde_json::Value = match serde_json::from_str(trimmed_end) {
Ok(v) => v, Err(_) => { debug!("failed to parse WS message as JSON");
continue; } }; if let Some(err) = json.get("error") { let code = err
.get("code").and_then(| c | c.as_i64()).unwrap_or(0); if code ==
AUTH_ERROR_CODE { let _ = auth_error_tx.send(()). await; return (false,
true); } tracing::warn!(error_code = code, "Server error (skipping)");
continue; } match json.get("method").and_then(| m | m.as_str()) {
Some(method) => tprintln!("acp_inbound::{}", method), None =>
tprintln!("ws_inbound::text"), } debug!(bytes = trimmed_end.len(),
"received WS text -> agent"); if to_agent_tx.send(trimmed_end
.to_string()).is_err() {
warn!("Failed to forward message to agent - channel closed"); break; } }
Ok(Message::Binary(bin)) => { tprintln!("ws_inbound::binary"); if let
Ok(s) = std::str::from_utf8(& bin) { let s = s.trim_end_matches(['\r',
'\n']); if s.is_empty() {
debug!("received empty WS binary frame - skipping"); continue; }
debug!(bytes = s.len(), "received WS binary(utf8) -> agent"); if
to_agent_tx.send(s.to_string()).is_err() { break; } } else {
debug!("received non-utf8 WS binary frame - skipping"); } }
Ok(Message::Close(frame_opt)) => { tprintln!("ws_inbound::close"); if let
Some(frame) = frame_opt { info!(code = ? frame.code, reason = % frame
.reason, "WS close received"); } else {
info!("WS close received (no frame)"); } break; } Ok(Message::Ping(p)) =>
{ tprintln!("ws_inbound::ping"); debug!(len = p.len(),
"received WS Ping"); } Ok(Message::Pong(p)) => {
tprintln!("ws_inbound::pong"); debug!(len = p.len(), "received WS Pong");
} Ok(Message::Frame(_)) => { tprintln!("ws_inbound::frame"); } Err(e) =>
{ tprintln!("ws_inbound::error::{:?}", & e); warn!(error = ? e,
"WS read error"); break; } } }
_ = cancel_read.cancelled() => break,
msg_res = tokio::time::timeout(liveness, ws_inbound.next()) => {
let Ok(msg_opt) = msg_res else {
// No frame (not even a pong for our keepalive pings)
// within the liveness window: the connection is dead
// or half-open. Break so the session ends and the
// reconnect loop takes over.
tprintln!("ws_inbound::liveness_timeout");
warn!(
timeout_secs = liveness.as_secs(),
"no WS traffic within liveness window, treating connection as dead"
);
xai_grok_telemetry::unified_log::warn(
"relay: read liveness timeout, reconnecting",
None,
Some(serde_json::json!({
"timeout_secs": liveness.as_secs(),
})),
);
break;
};
let Some(msg) = msg_opt else { break };
match msg {
Ok(Message::Text(text)) => {
let trimmed_end = text.trim_end_matches(['\r', '\n']);
if trimmed_end.is_empty() {
debug!("received empty/whitespace WS text frame - skipping");
continue;
}
let json: serde_json::Value = match serde_json::from_str(trimmed_end) {
Ok(v) => v,
Err(_) => {
debug!("failed to parse WS message as JSON");
continue;
}
};
if let Some(err) = json.get("error") {
let code = err.get("code").and_then(|c| c.as_i64()).unwrap_or(0);
if code == AUTH_ERROR_CODE {
// Signal auth error to the main loop
let _ = auth_error_tx.send(()).await;
return (false, true); // (normal_end, auth_error)
}
tracing::warn!(error_code = code, "Server error (skipping)");
continue;
}
match json.get("method").and_then(|m| m.as_str()) {
Some(method) => tprintln!("acp_inbound::{}", method),
None => tprintln!("ws_inbound::text"),
}
debug!(bytes = trimmed_end.len(), "received WS text -> agent");
if to_agent_tx.send(trimmed_end.to_string()).is_err() {
warn!("Failed to forward message to agent - channel closed");
break;
}
}
Ok(Message::Binary(bin)) => {
tprintln!("ws_inbound::binary");
if let Ok(s) = std::str::from_utf8(&bin) {
let s = s.trim_end_matches(['\r', '\n']);
if s.is_empty() {
debug!("received empty WS binary frame - skipping");
continue;
}
debug!(bytes = s.len(), "received WS binary(utf8) -> agent");
if to_agent_tx.send(s.to_string()).is_err() {
break;
}
} else {
debug!("received non-utf8 WS binary frame - skipping");
}
}
Ok(Message::Close(frame_opt)) => {
tprintln!("ws_inbound::close");
if let Some(frame) = frame_opt {
info!(code = ?frame.code, reason = %frame.reason, "WS close received");
} else {
info!("WS close received (no frame)");
}
break;
}
Ok(Message::Ping(p)) => {
tprintln!("ws_inbound::ping");
debug!(len = p.len(), "received WS Ping");
}
Ok(Message::Pong(p)) => {
tprintln!("ws_inbound::pong");
debug!(len = p.len(), "received WS Pong");
}
Ok(Message::Frame(_)) => {
tprintln!("ws_inbound::frame");
}
Err(e) => {
tprintln!("ws_inbound::error::{:?}", &e);
warn!(error = ?e, "WS read error");
break;
}
}
}
}
}
(true, false)
@ -509,33 +598,72 @@ where
let mut keepalive = tokio::time::interval(Duration::from_secs(KEEPALIVE_INTERVAL_SECS));
loop {
tokio::select! {
_ = cancel_write.cancelled() => break, msg_opt = from_agent_rx.recv() =>
{ match msg_opt { Some(msg) => { if
tracing::enabled!(tracing::Level::DEBUG) { if let Ok(json_val) =
serde_json::from_str::< serde_json::Value > (& msg) { let method =
json_val.get("method").and_then(| m | m.as_str()); let line_to_print =
match method { Some("session/update") => { let params = json_val
.get("params").unwrap_or(& serde_json::Value::Null);
format!("acp_outbound::session/update::{params}") } Some(m) =>
format!("acp_outbound::{m}"), None => "acp_outbound::response"
.to_string(), }; debug!("{line_to_print}"); } else {
debug!("acp_outbound::response"); } }
if ! msg.is_empty() && let Err(e) =
ws_outbound.send(Message::Text(Utf8Bytes::from(msg))). await {
warn!(error = ? e, "failed to send to WS"); break; } } None => {
info!("Agent outbound channel closed"); break; } } } _ = keepalive.tick()
=> { tprintln!("ws::keep_alive_tick"); if let Err(e) = ws_outbound
.send(Message::Ping(Vec::new().into())). await {
tprintln!("ws::keep_alive::error::{:?}", & e); break; } }
_ = cancel_write.cancelled() => break,
msg_opt = from_agent_rx.recv() => {
match msg_opt {
Some(msg) => {
// Per-message logging is debug-only: at info level a
// streaming session mirrors every `session/update`
// delta here, and the full JSON parse + params
// re-format produced >100 MB of leader.log churn on
// dashboard-heavy machines. Skip the parse entirely
// unless debug logging is enabled.
if tracing::enabled!(tracing::Level::DEBUG) {
if let Ok(json_val) =
serde_json::from_str::<serde_json::Value>(&msg)
{
let method = json_val.get("method").and_then(|m| m.as_str());
let line_to_print = match method {
Some("session/update") => {
let params = json_val
.get("params")
.unwrap_or(&serde_json::Value::Null);
format!("acp_outbound::session/update::{params}")
}
Some(m) => format!("acp_outbound::{m}"),
None => "acp_outbound::response".to_string(),
};
debug!("{line_to_print}");
} else {
debug!("acp_outbound::response");
}
}
if !msg.is_empty()
&& let Err(e) = ws_outbound.send(Message::Text(Utf8Bytes::from(msg))).await
{
warn!(error = ?e, "failed to send to WS");
break;
}
}
None => {
info!("Agent outbound channel closed");
break;
}
}
}
_ = keepalive.tick() => {
tprintln!("ws::keep_alive_tick");
if let Err(e) = ws_outbound.send(Message::Ping(Vec::new().into())).await {
tprintln!("ws::keep_alive::error::{:?}", &e);
break;
}
}
}
}
anyhow::Ok(())
};
tokio::select! {
(_, auth_error) = read_from_ws => {
info!("WebSocket read task completed (connection closed)"); if auth_error {
return Ok(SessionEndReason::AuthError); } } res = write_to_ws => {
info!("WebSocket write task completed"); res ?; }
info!("WebSocket read task completed (connection closed)");
if auth_error {
return Ok(SessionEndReason::AuthError);
}
}
res = write_to_ws => {
info!("WebSocket write task completed");
res?;
}
}
if auth_error_rx.try_recv().is_ok() {
return Ok(SessionEndReason::AuthError);
@ -591,10 +719,11 @@ mod tests {
let (_agent_out_tx, mut agent_out_rx) = mpsc::unbounded_channel::<String>();
let cancel = CancellationToken::new();
tokio::spawn(async move {
let auth_error = json!(
{ "jsonrpc" : "2.0", "id" : 1, "error" : { "code" : - 32000, "message" :
"Authentication required" } }
);
let auth_error = json!({
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32000, "message": "Authentication required" }
});
let _ = server_tx
.send(Message::Text(Utf8Bytes::from(auth_error.to_string())))
.await;
@ -617,10 +746,11 @@ mod tests {
let (_agent_out_tx, mut agent_out_rx) = mpsc::unbounded_channel::<String>();
let cancel = CancellationToken::new();
tokio::spawn(async move {
let other_error = json!(
{ "jsonrpc" : "2.0", "id" : 1, "error" : { "code" : - 32600, "message" :
"Invalid Request" } }
);
let other_error = json!({
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32600, "message": "Invalid Request" }
});
let _ = server_tx
.send(Message::Text(Utf8Bytes::from(other_error.to_string())))
.await;
@ -686,7 +816,7 @@ mod tests {
let cancel = CancellationToken::new();
tokio::spawn(async move {
for i in 0..12 {
let msg = json!({ "jsonrpc" : "2.0", "method" : "ping", "id" : i });
let msg = json!({ "jsonrpc": "2.0", "method": "ping", "id": i });
if server_tx
.send(Message::Text(Utf8Bytes::from(msg.to_string())))
.await
@ -725,9 +855,12 @@ mod tests {
let (to_agent_tx, mut to_agent_rx) = mpsc::unbounded_channel::<String>();
let (_agent_out_tx, mut agent_out_rx) = mpsc::unbounded_channel::<String>();
let cancel = CancellationToken::new();
let test_msg = json!(
{ "jsonrpc" : "2.0", "id" : 1, "method" : "initialize", "params" : {} }
);
let test_msg = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {}
});
let msg_str = test_msg.to_string();
tokio::spawn(async move {
let _ = server_tx
@ -955,10 +1088,11 @@ mod tests {
let (mut tx, _rx) = ws.split();
let n = count.fetch_add(1, Ordering::SeqCst);
if n == 0 {
let auth_err = json!(
{ "jsonrpc" : "2.0", "id" : 1, "error" : { "code" : - 32000,
"message" : "Token expired" } }
);
let auth_err = json!({
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32000, "message": "Token expired" }
});
let _ = tx
.send(Message::Text(Utf8Bytes::from(auth_err.to_string())))
.await;
@ -1010,10 +1144,11 @@ mod tests {
};
let (mut tx, _rx) = ws.split();
count.fetch_add(1, Ordering::SeqCst);
let auth_err = json!(
{ "jsonrpc" : "2.0", "id" : 1, "error" : { "code" : - 32000,
"message" : "Token expired" } }
);
let auth_err = json!({
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32000, "message": "Token expired" }
});
let _ = tx
.send(Message::Text(Utf8Bytes::from(auth_err.to_string())))
.await;

View file

@ -17,10 +17,11 @@ pub(crate) fn build_code_restore_meta(
) -> Option<Value> {
let decision = build_restore_decision(Some(target_sha), outcome, kind);
let summary = decision.summary?;
Some(serde_json::json!(
{ "restored" : decision.restored, "summary" : summary, "degree" : decision
.degree, }
))
Some(serde_json::json!({
"restored": decision.restored,
"summary": summary,
"degree": decision.degree,
}))
}
#[cfg(test)]
mod tests {

View file

@ -163,9 +163,33 @@ impl SubagentCoordinator {
subagent_usage_not_applied: self.subagent_usage_not_applied(prompt_id),
}
}
/// Drain all buffered completion summaries, returning them and clearing the buffer.
pub fn drain_pending_completions(&mut self) -> Vec<SubagentCompletionSummary> {
std::mem::take(&mut self.pending_completions)
pub fn drain_pending_completions_for(
&mut self,
session_id: &str,
) -> Vec<SubagentCompletionSummary> {
if session_id.is_empty() {
return std::mem::take(&mut self.pending_completions);
}
let (mine, others) = std::mem::take(&mut self.pending_completions)
.into_iter()
.partition(|c| {
c.owner_session_id.is_empty() || c.owner_session_id == session_id
});
self.pending_completions = others;
mine
}
pub fn discard_pending_completions_for(&mut self, session_id: &str) {
if session_id.is_empty() {
return;
}
self.pending_completions.retain(|c| c.owner_session_id != session_id);
}
fn enforce_pending_completions_cap(&mut self) {
const MAX_PENDING_COMPLETIONS: usize = 256;
if self.pending_completions.len() > MAX_PENDING_COMPLETIONS {
let excess = self.pending_completions.len() - MAX_PENDING_COMPLETIONS;
self.pending_completions.drain(..excess);
}
}
/// Collect references to subagents spawned for a specific parent prompt.
/// Returns only the children whose `parent_prompt_id` matches, so the
@ -300,6 +324,7 @@ impl SubagentCoordinator {
..Default::default()
};
let summary_output = result.output.clone();
let owner_session_id = parent_session_id.clone();
self.completed
.insert(
subagent_id.clone(),
@ -331,6 +356,7 @@ impl SubagentCoordinator {
self.pending_completions
.push(SubagentCompletionSummary {
subagent_id,
owner_session_id,
subagent_type,
description,
success: false,
@ -339,6 +365,7 @@ impl SubagentCoordinator {
turns: 0,
output: summary_output,
});
self.enforce_pending_completions_cap();
}
self.completion_notify.notify_waiters();
}
@ -425,15 +452,18 @@ impl SubagentCoordinator {
if success { "subagent completed" } else { "subagent failed" },
None,
Some(
serde_json::json!(
{ "subagent_id" : & completed.subagent_id, "subagent_type" : &
completed.subagent_type, "effective_model" : & completed
.effective_model_id, "success" : success, "cancelled" : completed
.result.cancelled, "duration_ms" : completed.result.duration_ms,
"turns" : completed.result.turns, "tool_calls" : completed.result
.tool_calls, "output_preview" : preview, "error" : & completed
.result.error, }
),
serde_json::json!({
"subagent_id": &completed.subagent_id,
"subagent_type": &completed.subagent_type,
"effective_model": &completed.effective_model_id,
"success": success,
"cancelled": completed.result.cancelled,
"duration_ms": completed.result.duration_ms,
"turns": completed.result.turns,
"tool_calls": completed.result.tool_calls,
"output_preview": preview,
"error": &completed.result.error,
}),
),
);
}
@ -441,6 +471,7 @@ impl SubagentCoordinator {
self.pending_completions
.push(SubagentCompletionSummary {
subagent_id: id.to_string(),
owner_session_id: completed.parent_session_id.clone(),
subagent_type: completed.subagent_type.clone(),
description: completed.description.clone(),
success,
@ -452,6 +483,7 @@ impl SubagentCoordinator {
completed.completion_output_cap,
),
});
self.enforce_pending_completions_cap();
}
if completed.persisted_output_dir.is_some() {
completed.result.output = Arc::from("");

View file

@ -118,8 +118,9 @@ pub(crate) async fn handle_subagent_request(
}
SubagentValidateTypeOutcome::NotAllowed { allowed } => {
let msg = format!(
"agent can only spawn: {}; '{}' not allowed", allowed.join(", "), request
.subagent_type
"agent can only spawn: {}; '{}' not allowed",
allowed.join(", "),
request.subagent_type
);
send_pre_spawn_failure(request, &msg, coordinator, &ctx, gateway);
return;
@ -201,7 +202,8 @@ pub(crate) async fn handle_subagent_request(
let prompt = request.prompt.clone();
if let Some(ref err) = effective_runtime.persona_error {
tracing::error!(
subagent_id = % request.id, error = err,
subagent_id = %request.id,
error = err,
"Persona resolution failed, aborting subagent spawn"
);
pending_guard.set_error(err.clone());
@ -210,7 +212,8 @@ pub(crate) async fn handle_subagent_request(
}
if let Some(ref warn) = effective_runtime.role_prompt_warning {
tracing::warn!(
subagent_id = % request.id, warning = warn,
subagent_id = %request.id,
warning = warn,
"Role prompt_file degraded, continuing without role prompt"
);
}
@ -252,7 +255,7 @@ pub(crate) async fn handle_subagent_request(
if let Some(ref source) = resume_source {
if request.runtime_overrides.model.is_some() {
tracing::debug!(
subagent_id = % request.id,
subagent_id = %request.id,
"Ignoring caller model override on resume; source model will be pinned"
);
}
@ -282,7 +285,7 @@ pub(crate) async fn handle_subagent_request(
&& source.worktree_path.is_none()
{
tracing::info!(
subagent_id = % request.id,
subagent_id = %request.id,
"Ignoring isolation=worktree override: resumed source had no worktree"
);
}
@ -310,15 +313,17 @@ pub(crate) async fn handle_subagent_request(
{
Ok(path) => {
tracing::info!(
subagent_id = % request.id, worktree_path = % path
.display(), snapshot_ref = % snapshot_ref,
subagent_id = %request.id,
worktree_path = %path.display(),
snapshot_ref = %snapshot_ref,
"Rehydrated subagent worktree from snapshot for resume"
);
Some(path)
}
Err(e) => {
tracing::warn!(
subagent_id = % request.id, error = % e,
subagent_id = %request.id,
error = %e,
"Failed to rehydrate subagent worktree, falling back to shared workspace"
);
None
@ -327,7 +332,8 @@ pub(crate) async fn handle_subagent_request(
}
ResumeWorktreeAction::Shared => {
tracing::warn!(
subagent_id = % request.id, worktree = % dest.display(),
subagent_id = %request.id,
worktree = %dest.display(),
"Resumed subagent worktree dir missing with no snapshot; using shared workspace"
);
None
@ -344,7 +350,8 @@ pub(crate) async fn handle_subagent_request(
Ok(base) => base.join(format!("subagent-{}", request.id)),
Err(e) => {
tracing::warn!(
subagent_id = % request.id, error = % e,
subagent_id = %request.id,
error = %e,
"Could not resolve worktree base dir, using temp dir for subagent worktree"
);
std::env::temp_dir().join("grok-subagent-worktrees").join(&request.id)
@ -374,22 +381,25 @@ pub(crate) async fn handle_subagent_request(
{
Ok(Ok(report)) => {
tracing::info!(
subagent_id = % request.id, worktree_path = % report.worktree_path
.display(), commit = % report.commit,
subagent_id = %request.id,
worktree_path = %report.worktree_path.display(),
commit = %report.commit,
"Created isolated worktree for subagent"
);
Some(report.worktree_path)
}
Ok(Err(e)) => {
tracing::warn!(
subagent_id = % request.id, error = % e,
subagent_id = %request.id,
error = %e,
"Failed to create worktree, falling back to shared workspace"
);
None
}
Err(e) => {
tracing::warn!(
subagent_id = % request.id, error = % e,
subagent_id = %request.id,
error = %e,
"Worktree creation task panicked, falling back to shared workspace"
);
None
@ -423,8 +433,9 @@ pub(crate) async fn handle_subagent_request(
|| effective_runtime.capability_mode.is_some()
{
tracing::info!(
subagent_id = % request.id, reasoning_effort = ? effective_runtime
.reasoning_effort, capability_mode = ? effective_runtime.capability_mode,
subagent_id = %request.id,
reasoning_effort = ?effective_runtime.reasoning_effort,
capability_mode = ?effective_runtime.capability_mode,
"Resolved runtime overrides for subagent"
);
}
@ -435,8 +446,9 @@ pub(crate) async fn handle_subagent_request(
if let Some(mode) = effective_runtime.capability_mode {
mode.filter_tool_config(&mut definition.tool_config);
tracing::info!(
subagent_id = % request.id, capability_mode = ? mode, tools_remaining =
definition.tool_config.tools.len(),
subagent_id = %request.id,
capability_mode = ?mode,
tools_remaining = definition.tool_config.tools.len(),
"Applied capability mode filter to agent tool config"
);
}
@ -446,7 +458,8 @@ pub(crate) async fn handle_subagent_request(
.unwrap_or(ctx.parent_depth + 1);
if strip_task_tools_at_max_depth(&mut definition.tool_config, child_depth) {
tracing::info!(
subagent_id = % request.id, child_depth,
subagent_id = %request.id,
child_depth,
"Stripped task tool from child at max depth"
);
}
@ -456,9 +469,9 @@ pub(crate) async fn handle_subagent_request(
.tools
.retain(|tool| {
!matches!(
tool.id.rsplit(':').next(), Some("scheduler_create" |
"scheduler_list" | "scheduler_delete")
)
tool.id.rsplit(':').next(),
Some("scheduler_create" | "scheduler_list" | "scheduler_delete")
)
});
}
if request.fork_context {
@ -483,8 +496,9 @@ pub(crate) async fn handle_subagent_request(
if model_unknown {
let (parent_config, parent_mid) = read_parent_sampling_config(&ctx).await;
tracing::warn!(
subagent_id = % request.id, resolved_model = % model_str, parent_model =
% parent_config.model,
subagent_id = %request.id,
resolved_model = %model_str,
parent_model = %parent_config.model,
"Resolved subagent model not found in available models — \
falling back to parent model"
);
@ -498,8 +512,10 @@ pub(crate) async fn handle_subagent_request(
{
if let Some(resolved) = resolve_model_override_to_config(source_model, &ctx) {
tracing::info!(
subagent_id = % request.id, resolved_model = % effective_model_id.0,
source_model = source_model, "Pinning resumed child to source model"
subagent_id = %request.id,
resolved_model = %effective_model_id.0,
source_model = source_model,
"Pinning resumed child to source model"
);
effective_sampling_config = resolved.0;
effective_model_id = resolved.1;
@ -523,9 +539,10 @@ pub(crate) async fn handle_subagent_request(
Ok(eff) => effective_sampling_config.reasoning_effort = Some(eff),
Err(err) => {
tracing::warn!(
value = raw, error = % err,
"subagent reasoning_effort: parse failed, ignoring override"
)
value = raw,
error = %err,
"subagent reasoning_effort: parse failed, ignoring override"
)
}
}
}
@ -574,7 +591,8 @@ pub(crate) async fn handle_subagent_request(
BootstrapInitialContext::Ready(ctx) => ctx,
BootstrapInitialContext::ResumeAbort(msg) => {
tracing::error!(
subagent_id = % request.id, error = % msg,
subagent_id = %request.id,
error = %msg,
"Resume-copy failed, aborting subagent spawn"
);
send_failure(request, &msg);
@ -824,18 +842,20 @@ pub(crate) async fn handle_subagent_request(
"subagent spawn credentials",
None,
Some(
serde_json::json!(
{ "subagent_id" : & request.id, "subagent_type" : & request
.subagent_type, "effective_model" : effective_model_id.0.as_ref(),
"effective_model_raw" : & effective_sampling_config.model, "base_url" : &
effective_sampling_config.base_url, "key_prefix" : key_prefix(&
effective_sampling_config.api_key), "auth_type" : format!("{:?}",
inherited_auth_type), "model_has_own_creds" : model_has_own_creds,
"auth_method_id" : ctx.auth_method_id.0.as_ref(), "parent_model" : ctx
.model_id.0.as_ref(), "parent_key_prefix" : key_prefix(& ctx
.sampling_config.api_key), "context_window" : effective_sampling_config
.context_window, }
),
serde_json::json!({
"subagent_id": &request.id,
"subagent_type": &request.subagent_type,
"effective_model": effective_model_id.0.as_ref(),
"effective_model_raw": &effective_sampling_config.model,
"base_url": &effective_sampling_config.base_url,
"key_prefix": key_prefix(&effective_sampling_config.api_key),
"auth_type": format!("{:?}", inherited_auth_type),
"model_has_own_creds": model_has_own_creds,
"auth_method_id": ctx.auth_method_id.0.as_ref(),
"parent_model": ctx.model_id.0.as_ref(),
"parent_key_prefix": key_prefix(&ctx.sampling_config.api_key),
"context_window": effective_sampling_config.context_window,
}),
),
);
let attribution_callback: Option<xai_grok_sampler::SharedAttributionCallback> = effective_sampling_config
@ -854,12 +874,13 @@ pub(crate) async fn handle_subagent_request(
if agent_permission_mode != definition.permission_mode {
if is_plugin_agent {
tracing::warn!(
agent = % definition.name, plugin = ? definition.plugin_name,
agent = %definition.name,
plugin = ?definition.plugin_name,
"ignoring permissionMode on plugin agent (not supported for security)"
);
} else {
tracing::warn!(
agent = % definition.name,
agent = %definition.name,
"ignoring subagent permissionMode=bypassPermissions: always-approve disabled by managed policy"
);
}
@ -868,8 +889,9 @@ pub(crate) async fn handle_subagent_request(
use xai_grok_tools::implementations::grok_build;
use xai_grok_tools::implementations::opencode;
let memory_tools: Vec<xai_grok_tools::registry::types::ToolConfig> = vec![
(& grok_build::ReadFileTool).into(), (& grok_build::SearchReplaceTool)
.into(), (& opencode::OpenCodeWriteTool).into(),
(&grok_build::ReadFileTool).into(),
(&grok_build::SearchReplaceTool).into(),
(&opencode::OpenCodeWriteTool).into(),
];
for tc in memory_tools {
if !definition.tool_config.tools.iter().any(|t| t.id == tc.id) {
@ -907,7 +929,8 @@ pub(crate) async fn handle_subagent_request(
if let Some(ref hooks_config) = definition.hooks {
if is_plugin_agent {
tracing::warn!(
agent = % definition.name, plugin = ? definition.plugin_name,
agent = %definition.name,
plugin = ?definition.plugin_name,
"ignoring hooks on plugin agent (not supported for security)"
);
} else if !crate::agent::folder_trust::agent_inline_hooks_allowed(
@ -915,7 +938,7 @@ pub(crate) async fn handle_subagent_request(
|| crate::agent::folder_trust::project_scope_allowed(&ctx.parent_cwd),
) {
tracing::warn!(
agent = % definition.name,
agent = %definition.name,
"ignoring hooks on untrusted project agent (folder not trusted; re-run with --trust)"
);
} else {
@ -926,9 +949,7 @@ pub(crate) async fn handle_subagent_request(
&ctx.parent_cwd,
);
for e in &errors {
tracing::warn!(
agent = % definition.name, error = ? e, "agent hook parse error"
);
tracing::warn!(agent = %definition.name, error = ?e, "agent hook parse error");
}
if !specs.is_empty() {
let specs: Vec<_> = specs
@ -953,7 +974,8 @@ pub(crate) async fn handle_subagent_request(
let agent_mcp_servers: Vec<_> = if is_plugin_agent {
if !definition.mcp_servers.is_empty() {
tracing::warn!(
agent = % definition.name, plugin = ? definition.plugin_name,
agent = %definition.name,
plugin = ?definition.plugin_name,
"ignoring mcpServers on plugin agent (not supported for security)"
);
}
@ -971,10 +993,7 @@ pub(crate) async fn handle_subagent_request(
})
.cloned()
.or_else(|| {
tracing::warn!(
agent = % definition.name, server = name,
"mcpServers: named ref not found in parent"
);
tracing::warn!(agent = %definition.name, server = name, "mcpServers: named ref not found in parent");
None
})
}
@ -992,10 +1011,7 @@ pub(crate) async fn handle_subagent_request(
>(serde_json::Value::Object(flat)) {
return Some(server);
}
tracing::debug!(
agent = % definition.name, server = name,
"ACP wire format parse failed, trying map-keyed"
);
tracing::debug!(agent = %definition.name, server = name, "ACP wire format parse failed, trying map-keyed");
}
if let Some(inner_obj) = config.as_object() {
let mut flat = inner_obj.clone();
@ -1009,10 +1025,7 @@ pub(crate) async fn handle_subagent_request(
return Some(server);
}
}
tracing::warn!(
agent = % definition.name, server = name,
"mcpServers: inline config could not be parsed"
);
tracing::warn!(agent = %definition.name, server = name, "mcpServers: inline config could not be parsed");
None
}
})
@ -1021,7 +1034,7 @@ pub(crate) async fn handle_subagent_request(
let parent_mcp_pool = if is_plugin_agent {
if ctx.parent_mcp_pool.is_some() {
tracing::debug!(
agent = % definition.name,
agent = %definition.name,
"skipping MCP pool inheritance for plugin agent"
);
}
@ -1040,7 +1053,8 @@ pub(crate) async fn handle_subagent_request(
.unwrap_or(0);
if mcp_inherited_count > 0 {
tracing::info!(
subagent_id = % request.id, mcp_count = mcp_inherited_count,
subagent_id = %request.id,
mcp_count = mcp_inherited_count,
"Subagent inherited MCP servers from parent pool"
);
}
@ -1064,7 +1078,8 @@ pub(crate) async fn handle_subagent_request(
};
if skills_inherited_count > 0 {
tracing::info!(
subagent_id = % request.id, skills_count = skills_inherited_count,
subagent_id = %request.id,
skills_count = skills_inherited_count,
"Subagent inherited skills from parent"
);
}
@ -1190,9 +1205,9 @@ pub(crate) async fn handle_subagent_request(
effective_model_id,
ctx.yolo_mode
|| matches!(
agent_permission_mode,
xai_grok_agent::config::PermissionMode::BypassPermissions
),
agent_permission_mode,
xai_grok_agent::config::PermissionMode::BypassPermissions
),
false,
None,
ctx.inference_idle_timeout_secs,
@ -1326,6 +1341,13 @@ pub(crate) async fn handle_subagent_request(
.send(SessionCommand::CopyFile {
respond_to: before_copy_tx,
});
if let Some(overrides) = ctx.inherited_tool_overrides.clone() {
let _ = child_handle
.cmd_tx
.send(SessionCommand::SetToolOverrides {
overrides,
});
}
let (prompt_tx, prompt_rx) = oneshot::channel();
let prompt_text = task_prompt_text;
let child_prompt_id = uuid::Uuid::now_v7().to_string();
@ -1334,9 +1356,7 @@ pub(crate) async fn handle_subagent_request(
.cmd_tx
.send(SessionCommand::Prompt {
prompt_id: child_prompt_id.clone(),
prompt_blocks: vec![
acp::ContentBlock::Text(acp::TextContent::new(prompt_text))
],
prompt_blocks: vec![acp::ContentBlock::Text(acp::TextContent::new(prompt_text))],
prompt_mode: crate::session::plan_mode::PromptMode::Agent,
artifact_upload_ctx: ctx
.gcs_bucket_url
@ -1365,6 +1385,7 @@ pub(crate) async fn handle_subagent_request(
json_schema: request.runtime_overrides.output_schema.clone(),
send_now: false,
admission: None,
tool_overrides_update: None,
respond_to: prompt_tx,
persist_ack: None,
parsed_prompt_tx: None,
@ -1401,9 +1422,11 @@ pub(crate) async fn handle_subagent_request(
}
};
tokio::select! {
biased; outcome = & mut fut => ForegroundWait::Done(outcome), _ =
parent_await_dropped => ForegroundWait::ParentGone, _ = budget =>
ForegroundWait::Budget,
// Bias to completion: a child finishing at the budget returns its real result.
biased;
outcome = &mut fut => ForegroundWait::Done(outcome),
_ = parent_await_dropped => ForegroundWait::ParentGone,
_ = budget => ForegroundWait::Budget,
}
};
match first {
@ -1417,14 +1440,14 @@ pub(crate) async fn handle_subagent_request(
parent_wait_guard.take();
if request.owner.is_workflow() {
tracing::info!(
subagent_id = % request.id, workflow_run_id = ? request.owner
.workflow_run_id(),
subagent_id = %request.id,
workflow_run_id = ?request.owner.workflow_run_id(),
"workflow subagent result receiver dropped; cancelling child",
);
cancel_token.cancel();
} else {
tracing::info!(
subagent_id = % request.id,
subagent_id = %request.id,
"foreground subagent await abandoned by its parent turn; detaching child to background (child keeps running)",
);
if !cancel_token.is_cancelled() {
@ -1436,8 +1459,8 @@ pub(crate) async fn handle_subagent_request(
}
ForegroundWait::Budget => {
tracing::info!(
subagent_id = % request.id, budget_ms = subagent_await_budget()
.as_millis() as u64,
subagent_id = %request.id,
budget_ms = subagent_await_budget().as_millis() as u64,
"foreground subagent exceeded await budget; auto-backgrounding (child keeps running)",
);
if let Some(tx) = result_tx.take() {
@ -1528,10 +1551,9 @@ pub(crate) async fn handle_subagent_request(
output: if final_text.is_empty() {
std::sync::Arc::from(
format!(
"Subagent '{}' ({}) was cancelled. {} tool calls, {} turns.",
request.description, request.subagent_type, tool_calls,
turns
),
"Subagent '{}' ({}) was cancelled. {} tool calls, {} turns.",
request.description, request.subagent_type, tool_calls, turns
),
)
} else {
std::sync::Arc::from(final_text)
@ -1568,10 +1590,9 @@ pub(crate) async fn handle_subagent_request(
output: if final_text.is_empty() {
std::sync::Arc::from(
format!(
"Subagent '{}' ({}) hit max-turns limit ({limit}). {} tool calls, {} turns.",
request.description, request.subagent_type, tool_calls,
turns
),
"Subagent '{}' ({}) hit max-turns limit ({limit}). {} tool calls, {} turns.",
request.description, request.subagent_type, tool_calls, turns
),
)
} else {
std::sync::Arc::from(final_text)
@ -1628,10 +1649,9 @@ pub(crate) async fn handle_subagent_request(
if final_text.is_empty() {
std::sync::Arc::from(
format!(
"Subagent '{}' ({}) completed successfully. {} tool calls, {} turns.",
request.description, request.subagent_type, tool_calls,
turns
),
"Subagent '{}' ({}) completed successfully. {} tool calls, {} turns.",
request.description, request.subagent_type, tool_calls, turns
),
)
} else {
std::sync::Arc::from(final_text)
@ -1877,13 +1897,15 @@ pub(crate) async fn handle_subagent_request(
{
Ok(_) => {
tracing::debug!(
subagent_id = % request.id, child_session_id = % child_session_id.0,
subagent_id = %request.id,
child_session_id = %child_session_id.0,
"Subagent trace artifacts uploaded"
);
}
Err(e) => {
tracing::warn!(
subagent_id = % request.id, error = % e,
subagent_id = %request.id,
error = %e,
"Subagent trace upload failed (non-fatal)"
);
}
@ -1958,7 +1980,8 @@ pub(crate) async fn handle_subagent_request(
};
if !fold_acked {
tracing::warn!(
subagent_id = % request.id, parent_prompt_id = ? request.parent_prompt_id,
subagent_id = %request.id,
parent_prompt_id = ?request.parent_prompt_id,
"subagent usage not applied; parent bill marked incomplete"
);
let sticky_prompt = request
@ -2030,10 +2053,10 @@ pub(crate) async fn handle_subagent_request(
}
(Some(_), None) | (None, Some(_)) => {
tracing::warn!(
child_session_id = % child_session_id.0, parent_session_id = % ctx
.parent_session_id, has_terminal_backend = ctx.parent_terminal_backend
.is_some(), has_notification_handle = ctx.parent_notification_handle
.is_some(),
child_session_id = %child_session_id.0,
parent_session_id = %ctx.parent_session_id,
has_terminal_backend = ctx.parent_terminal_backend.is_some(),
has_notification_handle = ctx.parent_notification_handle.is_some(),
"skipping reparent_notifications: parent_terminal_backend and \
parent_notification_handle must both be Some"
);
@ -2069,37 +2092,41 @@ pub(crate) async fn handle_subagent_request(
Ok(()) => {
worktree_removed = true;
tracing::info!(
subagent_id = % request.id, worktree_path = % wt_path
.display(), "snapshotted and removed subagent worktree"
subagent_id = %request.id,
worktree_path = %wt_path.display(),
"snapshotted and removed subagent worktree"
);
}
Err(e) => {
tracing::warn!(
subagent_id = % request.id, worktree_path = % wt_path
.display(), error = % e,
"snapshotted subagent worktree but removal failed; ref persisted for resume"
)
subagent_id = %request.id,
worktree_path = %wt_path.display(),
error = %e,
"snapshotted subagent worktree but removal failed; ref persisted for resume"
)
}
}
} else {
tracing::warn!(
subagent_id = % request.id, worktree_path = % wt_path
.display(),
subagent_id = %request.id,
worktree_path = %wt_path.display(),
"snapshot_ref not persisted; preserving worktree for resume"
);
}
}
Err(e) => {
tracing::warn!(
subagent_id = % request.id, worktree_path = % wt_path.display(),
error = % e,
subagent_id = %request.id,
worktree_path = %wt_path.display(),
error = %e,
"Failed to snapshot subagent worktree; preserving for review"
);
}
}
} else {
tracing::info!(
subagent_id = % request.id, worktree_path = % wt_path.display(),
subagent_id = %request.id,
worktree_path = %wt_path.display(),
"Worktree preserved for review"
);
}

View file

@ -166,6 +166,8 @@ pub(crate) struct SubagentSpawnContext {
pub auth: Option<crate::auth::GrokAuth>,
pub parent_cwd: PathBuf,
pub parent_session_id: String,
/// The parent's cutoff at spawn, applied to the child's first turn. `None` if unset.
pub inherited_tool_overrides: Option<xai_grok_sampling_types::ToolOverrides>,
pub yolo_mode: bool,
pub subagent_event_tx: mpsc::UnboundedSender<SubagentEvent>,
pub parent_depth: u32,
@ -929,13 +931,17 @@ fn log_subagent_model_resolution(
xai_grok_telemetry::unified_log::debug(
"subagent model resolved",
None,
Some(serde_json::json!(
{ "agent" : agent_name, "priority" : priority, "child_model" :
resolved_id.0.as_ref(), "child_base_url" : & resolved.base_url,
"child_key_prefix" : child_key, "parent_model" : & parent.model,
"parent_base_url" : & parent.base_url, "parent_key_prefix" : parent_key,
"keys_match" : keys_match, }
)),
Some(serde_json::json!({
"agent": agent_name,
"priority": priority,
"child_model": resolved_id.0.as_ref(),
"child_base_url": &resolved.base_url,
"child_key_prefix": child_key,
"parent_model": &parent.model,
"parent_base_url": &parent.base_url,
"parent_key_prefix": parent_key,
"keys_match": keys_match,
})),
);
}
/// Read the parent session's actual current sampling config.
@ -999,13 +1005,14 @@ async fn read_parent_sampling_config(
xai_grok_telemetry::unified_log::debug(
"subagent read parent config (live)",
None,
Some(serde_json::json!(
{ "parent_model" : & inherited.model, "parent_base_url" : &
inherited.base_url, "parent_key_prefix" : key_prefix(& inherited
.api_key), "session_model_id" : model_id.0.as_ref(),
"global_model_id" : global_model_id.0.as_ref(), "source" :
"chat_state", }
)),
Some(serde_json::json!({
"parent_model": &inherited.model,
"parent_base_url": &inherited.base_url,
"parent_key_prefix": key_prefix(&inherited.api_key),
"session_model_id": model_id.0.as_ref(),
"global_model_id": global_model_id.0.as_ref(),
"source": "chat_state",
})),
);
return (inherited, model_id);
}
@ -1017,12 +1024,13 @@ async fn read_parent_sampling_config(
xai_grok_telemetry::unified_log::warn(
"subagent read parent config (fallback)",
None,
Some(serde_json::json!(
{ "parent_model" : & ctx.sampling_config.model, "parent_base_url" : & ctx
.sampling_config.base_url, "parent_key_prefix" : key_prefix(& ctx
.sampling_config.api_key), "source" : "spawn_context_baseline",
"has_chat_state" : ctx.parent_chat_state.is_some(), }
)),
Some(serde_json::json!({
"parent_model": &ctx.sampling_config.model,
"parent_base_url": &ctx.sampling_config.base_url,
"parent_key_prefix": key_prefix(&ctx.sampling_config.api_key),
"source": "spawn_context_baseline",
"has_chat_state": ctx.parent_chat_state.is_some(),
})),
);
let mut fallback = ctx.sampling_config.clone();
fallback.supports_backend_search = ctx
@ -1080,14 +1088,17 @@ fn resolve_model_override_to_config(
xai_grok_telemetry::unified_log::debug(
"subagent resolve_model_override_to_config",
None,
Some(serde_json::json!(
{ "model_id" : model_id, "canonical_model" : canonical_model_id.0
.as_ref(), "resolved_model_raw" : & config.model, "base_url" : & config
.base_url, "key_prefix" : key_prefix(& config.api_key),
"has_own_credentials" : entry.has_own_credentials(), "has_session_key" :
has_session_key, "auth_type" : format!("{:?}", resolved_auth_type),
"auth_method_id" : ctx.auth_method_id.0.as_ref(), }
)),
Some(serde_json::json!({
"model_id": model_id,
"canonical_model": canonical_model_id.0.as_ref(),
"resolved_model_raw": &config.model,
"base_url": &config.base_url,
"key_prefix": key_prefix(&config.api_key),
"has_own_credentials": entry.has_own_credentials(),
"has_session_key": has_session_key,
"auth_type": format!("{:?}", resolved_auth_type),
"auth_method_id": ctx.auth_method_id.0.as_ref(),
})),
);
Some((config, canonical_model_id))
}
@ -1170,7 +1181,8 @@ fn conversation_tail_is_complete(
) -> bool {
use xai_grok_sampling_types::conversation::ConversationItem;
matches!(
items.last(), Some(ConversationItem::Assistant(a)) if a.tool_calls.is_empty()
items.last(),
Some(ConversationItem::Assistant(a)) if a.tool_calls.is_empty()
)
}
/// Decide the live-fork context.
@ -1263,10 +1275,7 @@ fn stamp_live_fork_session_metadata(
) {
let dir = session::persistence::session_dir(child_session_info);
if let Err(e) = std::fs::create_dir_all(&dir) {
tracing::warn!(
error = % e,
"live fork: could not create child session dir for metadata stamp"
);
tracing::warn!(error = %e, "live fork: could not create child session dir for metadata stamp");
return;
}
let summary_path = dir.join("summary.json");
@ -1288,7 +1297,7 @@ fn stamp_live_fork_session_metadata(
if let Ok(bytes) = serde_json::to_vec_pretty(summary)
&& let Err(e) = std::fs::write(&summary_path, bytes)
{
tracing::warn!(error = % e, "live fork: failed to write forked session summary");
tracing::warn!(error = %e, "live fork: failed to write forked session summary");
}
}
enum BootstrapInitialContext {
@ -1309,7 +1318,8 @@ async fn bootstrap_initial_context(
) -> BootstrapInitialContext {
if request.fork_context && request.resume_from.is_some() {
tracing::info!(
subagent_id = % request.id, resume_from = ? request.resume_from,
subagent_id = %request.id,
resume_from = ?request.resume_from,
resume_resolved = resume_source.is_some(),
"resume_from and fork_context both set; resolved resume wins (fail-closed on copy error, never forks)"
);
@ -1371,9 +1381,11 @@ async fn bootstrap_initial_context(
));
}
tracing::info!(
subagent_id = % request.id, source_subagent = % source.subagent_id,
chat_messages = result.chat_messages_copied, tool_state = result
.tool_state_copied, estimated_tokens,
subagent_id = %request.id,
source_subagent = %source.subagent_id,
chat_messages = result.chat_messages_copied,
tool_state = result.tool_state_copied,
estimated_tokens,
"Resume-copied source child session data into new child"
);
BootstrapInitialContext::Ready(resume_initial_context(conversation))
@ -1403,8 +1415,10 @@ async fn bootstrap_initial_context(
if let Some(items) = live_items {
let ctx_out = verbatim_or_normalize_fork(items, child_context_window);
tracing::info!(
subagent_id = % request.id, subagent_type = % request.subagent_type,
loaded_items = ctx_out.conversation.len(), source = ? ctx_out.source,
subagent_id = %request.id,
subagent_type = %request.subagent_type,
loaded_items = ctx_out.conversation.len(),
source = ?ctx_out.source,
verbatim = ctx_out.verbatim_fork,
"Forked context from live parent_chat_state"
);
@ -1445,16 +1459,17 @@ async fn bootstrap_initial_context(
return match storage.copy_session_data_sync(parent_info, child_session_info, copy_options) {
Ok(result) => {
tracing::info!(
subagent_id = % request.id, subagent_type = % request.subagent_type,
chat_messages = result.chat_messages_copied, tool_state = result
.tool_state_copied,
subagent_id = %request.id,
subagent_type = %request.subagent_type,
chat_messages = result.chat_messages_copied,
tool_state = result.tool_state_copied,
"Fork-copied parent session data into child (disk fallback)"
);
let items = storage
.load_chat_history_from_dir(child_session_dir)
.unwrap_or_else(|e| {
tracing::warn!(
error = % e,
error = %e,
"Failed to load forked chat history, starting with empty context"
);
vec![]
@ -1464,8 +1479,9 @@ async fn bootstrap_initial_context(
Err(e) => {
let err_msg = format!("{e}");
tracing::warn!(
subagent_id = % request.id, subagent_type = % request.subagent_type,
error = % e,
subagent_id = %request.id,
subagent_type = %request.subagent_type,
error = %e,
"Failed to fork-copy parent session, falling back to fresh"
);
BootstrapInitialContext::Ready(InitialContext {
@ -1479,7 +1495,8 @@ async fn bootstrap_initial_context(
};
}
tracing::warn!(
subagent_id = % request.id, subagent_type = % request.subagent_type,
subagent_id = %request.id,
subagent_type = %request.subagent_type,
"fork_context=true but no live parent conversation or parent_session_info; falling back to fresh"
);
BootstrapInitialContext::Ready(InitialContext {
@ -1550,7 +1567,8 @@ fn resume_inherited_cwd(source: Option<&ResumeSourceData>) -> Option<&str> {
}
if !Path::new(&source.child_cwd).is_dir() {
tracing::warn!(
source_subagent_id = % source.subagent_id, child_cwd = % source.child_cwd,
source_subagent_id = %source.subagent_id,
child_cwd = %source.child_cwd,
"Resume source cwd no longer exists; using parent workspace"
);
return None;
@ -1926,8 +1944,8 @@ async fn await_subagent_turn_or_cancellation(
cancel_token: CancellationToken,
) -> SubagentWaitOutcome {
tokio::select! {
_ = cancel_token.cancelled() => SubagentWaitOutcome::Cancelled, turn_result =
prompt_rx => SubagentWaitOutcome::TurnResult(Box::new(turn_result)),
_ = cancel_token.cancelled() => SubagentWaitOutcome::Cancelled,
turn_result = prompt_rx => SubagentWaitOutcome::TurnResult(Box::new(turn_result)),
}
}
/// Max time a blocking `spawn_subagent` may hold the turn before it is
@ -2048,6 +2066,7 @@ fn inject_subagent_completed_prompt(
}
let summary = SubagentCompletionSummary {
subagent_id: subagent_id.to_string(),
owner_session_id: request.parent_session_id.clone(),
subagent_type: request.subagent_type.clone(),
description: request.description.clone(),
success: result.success && !result.cancelled,
@ -2089,6 +2108,7 @@ fn inject_subagent_completed_prompt(
json_schema: None,
send_now: false,
admission: None,
tool_overrides_update: None,
respond_to,
persist_ack: None,
parsed_prompt_tx: None,
@ -2252,7 +2272,9 @@ async fn cancel_pending_subagent_at_promote(
&& let Err(e) = crate::session::worktree::remove_subagent_worktree(wt_path).await
{
tracing::warn!(
subagent_id, worktree_path = % wt_path.display(), error = % e,
subagent_id,
worktree_path = %wt_path.display(),
error = %e,
"failed to remove pristine worktree for killed-while-pending subagent"
);
}
@ -2384,7 +2406,8 @@ fn spawn_progress_publisher(
let heartbeat_max = tokio::time::Duration::from_secs(8);
loop {
tokio::select! {
_ = cancel_token.cancelled() => break, _ = interval.tick() => {}
_ = cancel_token.cancelled() => break,
_ = interval.tick() => {}
}
let signals = match signals_handle.snapshot().await {
Some(s) => s,
@ -2645,12 +2668,12 @@ fn write_subagent_meta(dir: &Path, meta: &SubagentMeta) -> bool {
let json = match serde_json::to_string_pretty(meta) {
Ok(json) => json,
Err(e) => {
tracing::warn!(error = % e, "failed to serialize subagent meta");
tracing::warn!(error = %e, "failed to serialize subagent meta");
return false;
}
};
if let Err(e) = atomic_write(&dir.join("meta.json"), &json) {
tracing::warn!(error = % e, "failed to write subagent meta");
tracing::warn!(error = %e, "failed to write subagent meta");
return false;
}
true
@ -2677,12 +2700,12 @@ fn write_subagent_output(dir: &Path, output: &str) -> bool {
let json = match serde_json::to_string(&file) {
Ok(json) => json,
Err(e) => {
tracing::warn!(error = % e, "failed to serialize subagent output");
tracing::warn!(error = %e, "failed to serialize subagent output");
return false;
}
};
if let Err(e) = atomic_write(&dir.join("output.json"), &json) {
tracing::warn!(error = % e, "failed to write subagent output");
tracing::warn!(error = %e, "failed to write subagent output");
return false;
}
true
@ -2693,7 +2716,7 @@ pub(crate) fn read_subagent_output(dir: &Path) -> Option<String> {
let file: SubagentOutputFile = match serde_json::from_str(&data) {
Ok(file) => file,
Err(e) => {
tracing::warn!(error = % e, "failed to parse subagent output.json");
tracing::warn!(error = %e, "failed to parse subagent output.json");
return None;
}
};
@ -2736,18 +2759,12 @@ fn update_subagent_meta_snapshot_ref(dir: &Path, snapshot_ref: &str, status: &st
Ok(data) => match serde_json::from_str::<SubagentMeta>(&data) {
Ok(meta) => meta,
Err(e) => {
tracing::warn!(
error = % e,
"failed to parse subagent meta; snapshot_ref not persisted (resume pointer lost)"
);
tracing::warn!(error = %e, "failed to parse subagent meta; snapshot_ref not persisted (resume pointer lost)");
return false;
}
},
Err(e) => {
tracing::warn!(
error = % e,
"failed to read subagent meta; snapshot_ref not persisted (resume pointer lost)"
);
tracing::warn!(error = %e, "failed to read subagent meta; snapshot_ref not persisted (resume pointer lost)");
return false;
}
};
@ -2908,13 +2925,15 @@ pub(crate) fn reconcile_orphaned_subagents(
Some(m) if m.status == "running" => {
if let Some(finish) = coordinator.completed_finish(&subagent_id) {
tracing::info!(
subagent_id = % subagent_id, parent_session_id,
subagent_id = %subagent_id,
parent_session_id,
"Re-emitting finish for completed subagent with a lost terminal meta write"
);
emit_subagent_notification(gateway, parent_session_id, finish, parent_cmd_tx);
} else {
tracing::info!(
subagent_id = % m.subagent_id, parent_session_id,
subagent_id = %m.subagent_id,
parent_session_id,
"Reconciling orphaned subagent left running by a previous process"
);
finalize_orphaned_subagent(&subagent_dir, m, gateway, parent_cmd_tx);
@ -2922,7 +2941,9 @@ pub(crate) fn reconcile_orphaned_subagents(
}
Some(m) => {
tracing::info!(
subagent_id = % subagent_id, parent_session_id, status = % m.status,
subagent_id = %subagent_id,
parent_session_id,
status = %m.status,
"Re-emitting finish for rewound subagent (terminal meta survived)"
);
emit_subagent_notification(
@ -2948,7 +2969,8 @@ pub(crate) fn reconcile_orphaned_subagents(
continue;
};
tracing::info!(
subagent_id = % subagent_id, parent_session_id,
subagent_id = %subagent_id,
parent_session_id,
"Reconciling inherited subagent with no local meta (cancelled)"
);
emit_subagent_notification(

File diff suppressed because it is too large Load diff

View file

@ -95,7 +95,7 @@ pub(crate) async fn single_check(
xai_grok_telemetry::unified_log::warn(
"paywall_check_error",
None,
Some(serde_json::json!({ "user_id" : user_id, "kind" : kind })),
Some(serde_json::json!({ "user_id": user_id, "kind": kind })),
);
return None;
}
@ -103,10 +103,10 @@ pub(crate) async fn single_check(
xai_grok_telemetry::unified_log::info(
"paywall_check_result",
None,
Some(serde_json::json!(
{ "user_id" : user_id, "subscription_tier" : user_info.subscription_tier,
}
)),
Some(serde_json::json!({
"user_id": user_id,
"subscription_tier": user_info.subscription_tier,
})),
);
let new_tier = match &user_info.subscription_tier {
Some(tier) if !tier.is_empty() => tier.clone(),
@ -118,7 +118,10 @@ pub(crate) async fn single_check(
xai_grok_telemetry::unified_log::info(
"paywall_check_subscription_detected",
None,
Some(serde_json::json!({ "user_id" : user_id, "new_tier" : new_tier, })),
Some(serde_json::json!({
"user_id": user_id,
"new_tier": new_tier,
})),
);
if let Err(e) = auth_manager
.refresh_chain(TokenType::OidcSession, RefreshReason::ServerRejected)
@ -127,10 +130,11 @@ pub(crate) async fn single_check(
xai_grok_telemetry::unified_log::warn(
"paywall_check_error",
None,
Some(serde_json::json!(
{ "user_id" : user_id, "kind" : "refresh_failed", "detail" : e
.to_string(), }
)),
Some(serde_json::json!({
"user_id": user_id,
"kind": "refresh_failed",
"detail": e.to_string(),
})),
);
}
let settings = if crate::util::config::resolve_remote_fetch_enabled() {
@ -149,7 +153,7 @@ pub(crate) async fn single_check(
xai_grok_telemetry::unified_log::info(
"paywall_check_unblocked",
None,
Some(serde_json::json!({ "user_id" : user_id, "new_tier" : new_tier })),
Some(serde_json::json!({ "user_id": user_id, "new_tier": new_tier })),
);
Some(UnblockResult { new_tier, settings })
}