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
|
|
@ -602,8 +602,7 @@ mod tests {
|
|||
item,
|
||||
ConversationItem::User(u) if u.content.iter().any(|p| matches!(
|
||||
p,
|
||||
ContentPart::Text { text }
|
||||
if text.as_ref() == IMAGE_COMPACT_PLACEHOLDER
|
||||
ContentPart::Text { text } if text.as_ref() == IMAGE_COMPACT_PLACEHOLDER
|
||||
))
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,8 +101,8 @@ pub fn truncate_trailing_incomplete_tool_call(
|
|||
mut conversation: Vec<ConversationItem>,
|
||||
) -> Vec<ConversationItem> {
|
||||
while matches!(
|
||||
conversation.last(), Some(ConversationItem::Assistant(a)) if ! a.tool_calls
|
||||
.is_empty()
|
||||
conversation.last(),
|
||||
Some(ConversationItem::Assistant(a)) if !a.tool_calls.is_empty()
|
||||
) {
|
||||
conversation.pop();
|
||||
}
|
||||
|
|
@ -176,7 +176,8 @@ fn recover_truncated_tail_unit(
|
|||
};
|
||||
}
|
||||
let owner = if matches!(
|
||||
body.last(), Some(ConversationItem::Assistant(a)) if ! a.tool_calls.is_empty()
|
||||
body.last(),
|
||||
Some(ConversationItem::Assistant(a)) if !a.tool_calls.is_empty()
|
||||
) {
|
||||
body.pop()
|
||||
} else {
|
||||
|
|
@ -1065,9 +1066,11 @@ mod tests {
|
|||
}
|
||||
#[test]
|
||||
fn compaction_attempt_defaults_optional_fields_for_old_artifacts() {
|
||||
let json = serde_json::json!(
|
||||
{ "attempt" : 1, "outcome" : "transient", "summary_chars" : 0, }
|
||||
);
|
||||
let json = serde_json::json!({
|
||||
"attempt": 1,
|
||||
"outcome": "transient",
|
||||
"summary_chars": 0,
|
||||
});
|
||||
let parsed: CompactionAttempt = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(parsed.summary, None);
|
||||
assert_eq!(parsed.error, None);
|
||||
|
|
@ -1322,6 +1325,7 @@ actual user question";
|
|||
"<user_info>OS: macos</user_info>\n<user_query>\nreal task\n</user_query>",
|
||||
),
|
||||
ConversationItem::assistant("done"),
|
||||
// This is what run_inline_auto_continue() pushes after compaction:
|
||||
ConversationItem::user(AUTO_CONTINUE_PROMPT),
|
||||
ConversationItem::assistant("continuing..."),
|
||||
];
|
||||
|
|
@ -2240,7 +2244,9 @@ actual user question";
|
|||
let items = vec![
|
||||
ConversationItem::system("sys"),
|
||||
ConversationItem::user("prompt"),
|
||||
// Orphaned tool result — no assistant with matching tool_calls
|
||||
ConversationItem::tool_result("call_ORPHAN", "result"),
|
||||
// Valid pair
|
||||
ConversationItem::assistant_tool_calls(vec![ToolCall {
|
||||
id: "call_VALID".into(),
|
||||
name: "read_file".to_string(),
|
||||
|
|
@ -2354,6 +2360,7 @@ actual user question";
|
|||
let mut items = vec![
|
||||
ConversationItem::system("sys"),
|
||||
ConversationItem::user("prompt"),
|
||||
// ← the assistant declaring call_LOST is missing here
|
||||
ConversationItem::tool_result("call_LOST", "orphaned result"),
|
||||
ConversationItem::assistant_tool_calls(vec![call("call_OK")]),
|
||||
ConversationItem::tool_result("call_OK", "fine"),
|
||||
|
|
@ -2427,6 +2434,7 @@ actual user question";
|
|||
ConversationItem::tool_result("call_A", "ok"),
|
||||
ConversationItem::assistant_tool_calls(vec![call("call_C")]),
|
||||
ConversationItem::tool_result("call_C", "ok"),
|
||||
// call_B's owner was flushed two messages ago.
|
||||
ConversationItem::tool_result("call_B", "displaced"),
|
||||
];
|
||||
let report = repair_history(&mut items);
|
||||
|
|
@ -2678,13 +2686,18 @@ actual user question";
|
|||
async fn build_compacted_history_multi_turn_with_parallel_tool_calls() {
|
||||
use xai_grok_sampling_types::{AssistantItem, ToolCall};
|
||||
let conversation = vec![
|
||||
// [0] System prompt
|
||||
ConversationItem::system("You are a helpful coding assistant."),
|
||||
// [1] User info prefix (no <user_query> tags — this is the initial message)
|
||||
ConversationItem::user(
|
||||
"<user_info>\nOS Version: macos\nShell: /bin/bash\nWorkspace Path: /Users/dev/project\n</user_info>\n\n<project_layout>\n/Users/dev/project/\n src/\n main.rs\n lib.rs\n</project_layout>",
|
||||
),
|
||||
// ── Turn 1 ──────────────────────────────────────────────────
|
||||
// [2] User query (wrapped in <user_query> tags by parse_prompt)
|
||||
ConversationItem::user(
|
||||
"<user_query>\nRead main.rs and lib.rs and tell me what they do\n</user_query>",
|
||||
),
|
||||
// [3] Assistant with 2 parallel tool calls
|
||||
ConversationItem::Assistant(AssistantItem {
|
||||
content: "I'll read both files for you.".into(),
|
||||
tool_calls: vec![
|
||||
|
|
@ -2703,20 +2716,26 @@ actual user question";
|
|||
model_fingerprint: None,
|
||||
reasoning_effort: None,
|
||||
}),
|
||||
// [4] Tool result for call_1
|
||||
ConversationItem::tool_result(
|
||||
"call_1",
|
||||
"fn main() {\n println!(\"hello world\");\n}",
|
||||
),
|
||||
// [5] Tool result for call_2
|
||||
ConversationItem::tool_result(
|
||||
"call_2",
|
||||
"pub fn add(a: i32, b: i32) -> i32 {\n a + b\n}",
|
||||
),
|
||||
// [6] Assistant summary after reading both files
|
||||
ConversationItem::assistant(
|
||||
"main.rs prints hello world. lib.rs has an `add` function.",
|
||||
),
|
||||
// ── Turn 2 ──────────────────────────────────────────────────
|
||||
// [7] User query (second turn)
|
||||
ConversationItem::user(
|
||||
"<user_query>\nNow fix the typo in main.rs and run the tests\n</user_query>",
|
||||
),
|
||||
// [8] Assistant with 2 parallel tool calls
|
||||
ConversationItem::Assistant(AssistantItem {
|
||||
content: "I'll fix the typo and run tests.".into(),
|
||||
tool_calls: vec![
|
||||
|
|
@ -2736,11 +2755,14 @@ actual user question";
|
|||
model_fingerprint: None,
|
||||
reasoning_effort: None,
|
||||
}),
|
||||
// [9] Tool result for call_3
|
||||
ConversationItem::tool_result("call_3", "File edited successfully."),
|
||||
// [10] Tool result for call_4
|
||||
ConversationItem::tool_result(
|
||||
"call_4",
|
||||
"running 1 test\ntest tests::test_add ... ok\n\ntest result: ok. 1 passed",
|
||||
),
|
||||
// [11] Assistant final response
|
||||
ConversationItem::assistant("Fixed the typo and all tests pass!"),
|
||||
];
|
||||
let mut edited = BTreeSet::new();
|
||||
|
|
@ -2782,8 +2804,7 @@ actual user question";
|
|||
});
|
||||
assert_eq!(compacted.len(), 9, "compacted history should have 9 items");
|
||||
assert!(
|
||||
matches!(& compacted[0], ConversationItem::System(s) if s.content.as_ref() ==
|
||||
"You are a helpful coding assistant.")
|
||||
matches!(&compacted[0], ConversationItem::System(s) if s.content.as_ref() == "You are a helpful coding assistant.")
|
||||
);
|
||||
let prefix = compacted[1].text_content();
|
||||
assert!(
|
||||
|
|
@ -2948,8 +2969,7 @@ The user asked to read main.rs and lib.rs. main.rs prints hello world, lib.rs ha
|
|||
summary_count: 1,
|
||||
});
|
||||
assert!(!compacted.iter().any(|item| {
|
||||
matches!(item, ConversationItem::User(user)
|
||||
if user.synthetic_reason == Some(SyntheticReason::ProjectInstructions))
|
||||
matches!(item, ConversationItem::User(user) if user.synthetic_reason == Some(SyntheticReason::ProjectInstructions))
|
||||
}));
|
||||
}
|
||||
/// The AGENTS.md slot must use the structural project-instructions tag.
|
||||
|
|
@ -3022,8 +3042,9 @@ The user asked to read main.rs and lib.rs. main.rs prints hello world, lib.rs ha
|
|||
});
|
||||
let has_project_instructions = compacted.iter().any(|item| {
|
||||
matches!(
|
||||
item, ConversationItem::User(u) if u.synthetic_reason ==
|
||||
Some(SyntheticReason::ProjectInstructions)
|
||||
item,
|
||||
ConversationItem::User(u)
|
||||
if u.synthetic_reason == Some(SyntheticReason::ProjectInstructions)
|
||||
)
|
||||
});
|
||||
assert!(
|
||||
|
|
@ -3392,11 +3413,9 @@ The user asked to read main.rs and lib.rs. main.rs prints hello world, lib.rs ha
|
|||
ConversationItem::tool_result("c1", "fn main() {}"),
|
||||
];
|
||||
let has_tool_calls = |items: &[ConversationItem]| {
|
||||
items.iter().any(|i| {
|
||||
matches!(
|
||||
i, ConversationItem::Assistant(a) if ! a.tool_calls.is_empty()
|
||||
)
|
||||
})
|
||||
items
|
||||
.iter()
|
||||
.any(|i| matches!(i, ConversationItem::Assistant(a) if !a.tool_calls.is_empty()))
|
||||
};
|
||||
let has_tool_result = |items: &[ConversationItem]| {
|
||||
items
|
||||
|
|
@ -3405,10 +3424,8 @@ The user asked to read main.rs and lib.rs. main.rs prints hello world, lib.rs ha
|
|||
};
|
||||
let has_image = |items: &[ConversationItem]| {
|
||||
items.iter().any(|i| {
|
||||
matches!(
|
||||
i, ConversationItem::User(u) if u.content.iter().any(| p |
|
||||
matches!(p, ContentPart::Image { .. }))
|
||||
)
|
||||
matches!(i, ConversationItem::User(u)
|
||||
if u.content.iter().any(|p| matches!(p, ContentPart::Image { .. })))
|
||||
})
|
||||
};
|
||||
let seg = prepare_conversation_for_segment(conv.clone());
|
||||
|
|
@ -3514,6 +3531,7 @@ The user asked to read main.rs and lib.rs. main.rs prints hello world, lib.rs ha
|
|||
arguments: r#"{"target_file":"a.rs"}"#.into(),
|
||||
}]),
|
||||
ConversationItem::tool_result("c1", "fn main() {}"),
|
||||
// Trailing, no matching ToolResult — results never arrived.
|
||||
ConversationItem::assistant_tool_calls(vec![ToolCall {
|
||||
id: "c2".into(),
|
||||
name: "grep".to_string(),
|
||||
|
|
@ -3564,8 +3582,8 @@ The user asked to read main.rs and lib.rs. main.rs prints hello world, lib.rs ha
|
|||
let big = "x".repeat(800);
|
||||
let conv = vec![
|
||||
ConversationItem::system("sys"),
|
||||
ConversationItem::user(&big),
|
||||
ConversationItem::assistant(&big),
|
||||
ConversationItem::user(&big), // old + large -> dropped
|
||||
ConversationItem::assistant(&big), // old + large -> dropped
|
||||
ConversationItem::user("recent question"),
|
||||
ConversationItem::assistant("recent answer"),
|
||||
];
|
||||
|
|
@ -3620,7 +3638,7 @@ The user asked to read main.rs and lib.rs. main.rs prints hello world, lib.rs ha
|
|||
name: "read_file".to_string(),
|
||||
arguments: "{}".into(),
|
||||
}]),
|
||||
ConversationItem::tool_result("c1", huge.as_str()),
|
||||
ConversationItem::tool_result("c1", huge.as_str()), // triggering result
|
||||
];
|
||||
let out = fit_conversation_to_budget(conv, 100);
|
||||
let tr = out
|
||||
|
|
@ -3640,8 +3658,7 @@ The user asked to read main.rs and lib.rs. main.rs prints hello world, lib.rs ha
|
|||
);
|
||||
assert!(
|
||||
out.iter()
|
||||
.any(|i| matches!(i, ConversationItem::Assistant(a) if ! a
|
||||
.tool_calls.is_empty())),
|
||||
.any(|i| matches!(i, ConversationItem::Assistant(a) if !a.tool_calls.is_empty())),
|
||||
"owning assistant tool_use must be kept so the result is not orphaned"
|
||||
);
|
||||
let est: u64 = out.iter().map(estimate_item_tokens).sum();
|
||||
|
|
@ -3678,15 +3695,17 @@ The user asked to read main.rs and lib.rs. main.rs prints hello world, lib.rs ha
|
|||
}
|
||||
let conv = vec![
|
||||
ConversationItem::system("sys"),
|
||||
img_user,
|
||||
img_user, // old turn, huge by image charges, ~0 by text bytes
|
||||
ConversationItem::user("recent question"),
|
||||
ConversationItem::assistant("recent answer"),
|
||||
];
|
||||
let out = fit_conversation_to_budget(conv, 1_000);
|
||||
assert!(
|
||||
!out.iter()
|
||||
.any(|i| matches!(i, ConversationItem::User(u) if u.content
|
||||
.iter().any(| p | matches!(p, ContentPart::Image { .. })))),
|
||||
!out.iter().any(|i| matches!(
|
||||
i,
|
||||
ConversationItem::User(u)
|
||||
if u.content.iter().any(|p| matches!(p, ContentPart::Image { .. }))
|
||||
)),
|
||||
"image-heavy old turn must be counted (765/image) and trimmed, not kept"
|
||||
);
|
||||
assert!(
|
||||
|
|
@ -3708,7 +3727,7 @@ The user asked to read main.rs and lib.rs. main.rs prints hello world, lib.rs ha
|
|||
});
|
||||
let conv = vec![
|
||||
ConversationItem::system("sys"),
|
||||
reasoning,
|
||||
reasoning, // old turn, huge by encrypted bytes, 0 by visible text
|
||||
ConversationItem::user("recent question"),
|
||||
ConversationItem::assistant("recent answer"),
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in a new issue