Fix child startup readiness and ordered provider tools

Wait for shell bootstrap before dispatching child prompts, serialize provider preprocessing, and make permission callbacks idempotent. Restrict task lists to concrete multistep plans and stop inferring completion from tool activity.

Update regression fixtures and resolve existing lint and test-layout failures. Verified formatting, both presubmit Clippy commands, and 354 targeted nextest tests.
This commit is contained in:
2026-09-09 13:55:34 -05:00
parent 4b703c019f
commit 1845b9e20e
41 changed files with 1097 additions and 895 deletions
@@ -1044,6 +1044,9 @@ impl ProviderRun {
call.state = PendingToolCallState::PermissionPending { request };
Ok(())
}
PendingToolCallState::PermissionPending { request: pending } if pending == &request => {
Ok(())
}
PendingToolCallState::Resolved { .. } => {
Err(ProviderRunProtocolError::DuplicateToolUpdate {
call_id: call.call.id.clone(),
@@ -1068,6 +1071,12 @@ impl ProviderRun {
let call = self.pending_tool_call_mut(work_id, call_id)?;
let pending_request_id = match &call.state {
PendingToolCallState::PermissionPending { request } => request.id.clone(),
PendingToolCallState::Approved {
request_id: approved_request_id,
decision: approved_decision,
} if approved_request_id == request_id && approved_decision == &decision => {
return Ok(());
}
PendingToolCallState::Resolved { .. } => {
return Err(ProviderRunProtocolError::DuplicateToolUpdate {
call_id: call.call.id.clone(),
@@ -528,6 +528,61 @@ fn exact_batch_submission_rejects_missing_duplicate_and_unknown_results_without_
assert_eq!(run, before);
}
#[test]
fn repeated_permission_notifications_preserve_pending_sibling_tools() {
let mut run = run();
let batch = accept_tool_turn(
&mut run,
tool_turn(
vec![
tool_call("first", "run_shell_command"),
tool_call("second", "run_shell_command"),
],
&["run_shell_command"],
),
);
let request = PermissionRequest {
id: "permission-first".to_string(),
call_id: "first".to_string(),
kind: PermissionKind::Execute,
reason: Some("run a command".to_string()),
};
run.request_tool_permission(&batch.work_id, request.clone())
.unwrap();
run.request_tool_permission(&batch.work_id, request.clone())
.unwrap();
let mut conflicting = request;
conflicting.id = "different-permission".to_string();
assert!(
run.request_tool_permission(&batch.work_id, conflicting)
.is_err()
);
for _ in 0..2 {
run.resolve_tool_permission(
&batch.work_id,
"first",
"permission-first",
PermissionDecision::AllowOnce,
)
.unwrap();
}
assert!(
run.resolve_tool_permission(
&batch.work_id,
"first",
"permission-first",
PermissionDecision::AlwaysAllow,
)
.is_err()
);
run.start_tool(&batch.work_id, "first").unwrap();
let ProviderRunState::AwaitingTools { batch } = run.state() else {
panic!("permission notifications must leave the batch pending");
};
assert_eq!(batch.calls[0].state, PendingToolCallState::Executing);
assert_eq!(batch.calls[1].state, PendingToolCallState::Proposed);
}
#[test]
fn permission_denial_becomes_one_correlated_result() {
let mut run = run();