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:
parent
3af4d5d398
commit
a5727c5960
482 changed files with 37627 additions and 13402 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(¶ms).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(¶ms).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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
File diff suppressed because it is too large
Load diff
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue