Skip to content

Commit 6c93338

Browse files
committed
Revert "Defer Claude input during tool calls"
This reverts commit 5935c7e.
1 parent 5935c7e commit 6c93338

5 files changed

Lines changed: 27 additions & 379 deletions

File tree

AGENTS.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,6 @@ from a real chat captured during local debugging.
4040
- Normal streaming completion should close stdin and wait for the Claude CLI
4141
process to exit; force-closing the transport can race Claude's local
4242
transcript flush and break later `--resume` behavior.
43-
- If a follow-up user input arrives while Claude has emitted `tool_use` but not
44-
the matching `tool_result`, accept it in Garyx but defer writing it to Claude
45-
stdin until the tool-result boundary. Sending during that gap can make
46-
Claude's own transcript contain a clipped assistant continuation.
4743
- Claude SDK approval tests need a `can_use_tool` callback, which adds
4844
`--permission-prompt-tool stdio`; changing only `permission_mode` may not
4945
make Claude send `can_use_tool` requests to the SDK.

CLAUDE.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,6 @@ from a real chat captured during local debugging.
4040
- Normal streaming completion should close stdin and wait for the Claude CLI
4141
process to exit; force-closing the transport can race Claude's local
4242
transcript flush and break later `--resume` behavior.
43-
- If a follow-up user input arrives while Claude has emitted `tool_use` but not
44-
the matching `tool_result`, accept it in Garyx but defer writing it to Claude
45-
stdin until the tool-result boundary. Sending during that gap can make
46-
Claude's own transcript contain a clipped assistant continuation.
4743
- Claude SDK approval tests need a `can_use_tool` callback, which adds
4844
`--permission-prompt-tool stdio`; changing only `permission_mode` may not
4945
make Claude send `can_use_tool` requests to the SDK.

garyx-bridge/src/claude_provider.rs

Lines changed: 25 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -467,30 +467,10 @@ fn extract_tool_session_messages(
467467
}
468468
}
469469

470-
fn count_tool_use_blocks(blocks: &[ContentBlock]) -> usize {
470+
fn has_tool_result_blocks(blocks: &[ContentBlock]) -> bool {
471471
blocks
472472
.iter()
473-
.filter(|block| matches!(block, ContentBlock::ToolUse(_)))
474-
.count()
475-
}
476-
477-
fn count_tool_result_blocks(blocks: &[ContentBlock]) -> usize {
478-
blocks
479-
.iter()
480-
.filter(|block| matches!(block, ContentBlock::ToolResult(_)))
481-
.count()
482-
}
483-
484-
fn count_user_tool_result_messages(user_msg: &claude_agent_sdk::UserMessage) -> usize {
485-
let block_count = match user_msg.content {
486-
claude_agent_sdk::UserContent::Blocks(ref blocks) => count_tool_result_blocks(blocks),
487-
claude_agent_sdk::UserContent::Text(_) => 0,
488-
};
489-
if block_count > 0 {
490-
block_count
491-
} else {
492-
usize::from(user_msg.tool_use_result.is_some())
493-
}
473+
.any(|b| matches!(b, ContentBlock::ToolResult(_)))
494474
}
495475

496476
fn metadata_string_map(
@@ -663,13 +643,9 @@ pub struct ClaudeCliProvider {
663643
run_session_map: Mutex<HashMap<String, String>>,
664644
/// User inputs accepted but not yet completed per live run.
665645
run_pending_inputs: Mutex<HashMap<String, VecDeque<PendingAckMarker>>>,
666-
/// User inputs accepted while Claude is between tool_use and tool_result.
667-
run_input_states: Mutex<HashMap<String, RunInputState>>,
668646
/// Last user message per thread, used for auto-recovery replay.
669647
last_messages: Mutex<HashMap<String, String>>,
670648
#[cfg(test)]
671-
test_sent_streaming_inputs: Mutex<Vec<(String, OutboundUserMessage)>>,
672-
#[cfg(test)]
673649
test_run_attempts: Mutex<VecDeque<Result<Option<SdkRunOutcome>, BridgeError>>>,
674650
#[cfg(test)]
675651
test_recorded_session_attempts: Mutex<Vec<Option<String>>>,
@@ -682,18 +658,6 @@ enum PendingAckMarker {
682658
QueuedInput(String),
683659
}
684660

685-
#[derive(Debug, Clone)]
686-
struct PendingClaudeInput {
687-
pending_input_id: String,
688-
outbound: OutboundUserMessage,
689-
}
690-
691-
#[derive(Debug, Default)]
692-
struct RunInputState {
693-
outstanding_tool_uses: usize,
694-
queued_inputs: VecDeque<PendingClaudeInput>,
695-
}
696-
697661
impl ClaudeCliProvider {
698662
/// Create a new provider with the given config.
699663
pub fn new(config: ClaudeCodeConfig) -> Self {
@@ -704,11 +668,8 @@ impl ClaudeCliProvider {
704668
active_runs: Mutex::new(HashMap::new()),
705669
run_session_map: Mutex::new(HashMap::new()),
706670
run_pending_inputs: Mutex::new(HashMap::new()),
707-
run_input_states: Mutex::new(HashMap::new()),
708671
last_messages: Mutex::new(HashMap::new()),
709672
#[cfg(test)]
710-
test_sent_streaming_inputs: Mutex::new(Vec::new()),
711-
#[cfg(test)]
712673
test_run_attempts: Mutex::new(VecDeque::new()),
713674
#[cfg(test)]
714675
test_recorded_session_attempts: Mutex::new(Vec::new()),
@@ -920,18 +881,13 @@ impl ClaudeCliProvider {
920881
.lock()
921882
.await
922883
.insert(run_id.to_owned(), thread_id.to_owned());
923-
self.run_input_states
924-
.lock()
925-
.await
926-
.insert(run_id.to_owned(), RunInputState::default());
927884
}
928885

929886
/// Unregister a run after completion and return handle for cleanup.
930887
async fn unregister_run(&self, run_id: &str) -> (Option<ClaudeRunControl>, Option<String>) {
931888
let run = self.active_runs.lock().await.remove(run_id);
932889
let thread_id = self.run_session_map.lock().await.remove(run_id);
933890
self.run_pending_inputs.lock().await.remove(run_id);
934-
self.run_input_states.lock().await.remove(run_id);
935891
(run, thread_id)
936892
}
937893

@@ -942,124 +898,6 @@ impl ClaudeCliProvider {
942898
}
943899
}
944900

945-
async fn note_tool_uses_started(&self, run_id: &str, count: usize) {
946-
if count == 0 {
947-
return;
948-
}
949-
if let Some(state) = self.run_input_states.lock().await.get_mut(run_id) {
950-
state.outstanding_tool_uses = state.outstanding_tool_uses.saturating_add(count);
951-
}
952-
}
953-
954-
async fn note_tool_results_finished(&self, run_id: &str, count: usize) {
955-
if count == 0 {
956-
return;
957-
}
958-
if let Some(state) = self.run_input_states.lock().await.get_mut(run_id) {
959-
state.outstanding_tool_uses = state.outstanding_tool_uses.saturating_sub(count);
960-
}
961-
}
962-
963-
async fn send_streaming_input_now(
964-
&self,
965-
run_id: &str,
966-
pending_input_id: &str,
967-
outbound: OutboundUserMessage,
968-
) -> bool {
969-
let run = { self.active_runs.lock().await.get(run_id).cloned() };
970-
if let Some(run) = run {
971-
match run.send_user_message(outbound).await {
972-
Ok(()) => return true,
973-
Err(e) => {
974-
self.rollback_pending_input(run_id, pending_input_id).await;
975-
tracing::warn!(
976-
run_id = %run_id,
977-
pending_input_id = %pending_input_id,
978-
error = %e,
979-
"failed to send queued Claude streaming input"
980-
);
981-
return false;
982-
}
983-
}
984-
}
985-
986-
#[cfg(test)]
987-
{
988-
self.test_sent_streaming_inputs
989-
.lock()
990-
.await
991-
.push((run_id.to_owned(), outbound));
992-
return true;
993-
}
994-
995-
#[cfg(not(test))]
996-
{
997-
self.rollback_pending_input(run_id, pending_input_id).await;
998-
false
999-
}
1000-
}
1001-
1002-
async fn send_or_queue_streaming_input(
1003-
&self,
1004-
run_id: &str,
1005-
pending_input_id: String,
1006-
outbound: OutboundUserMessage,
1007-
) -> bool {
1008-
let pending = PendingClaudeInput {
1009-
pending_input_id,
1010-
outbound,
1011-
};
1012-
1013-
{
1014-
let mut states = self.run_input_states.lock().await;
1015-
if let Some(state) = states.get_mut(run_id)
1016-
&& state.outstanding_tool_uses > 0
1017-
{
1018-
state.queued_inputs.push_back(pending);
1019-
tracing::debug!(
1020-
run_id = %run_id,
1021-
outstanding_tool_uses = state.outstanding_tool_uses,
1022-
queued_inputs = state.queued_inputs.len(),
1023-
"deferring Claude streaming input until tool results are complete"
1024-
);
1025-
return true;
1026-
}
1027-
}
1028-
1029-
let PendingClaudeInput {
1030-
pending_input_id,
1031-
outbound,
1032-
} = pending;
1033-
self.send_streaming_input_now(run_id, &pending_input_id, outbound)
1034-
.await
1035-
}
1036-
1037-
async fn flush_queued_streaming_inputs_if_safe(&self, run_id: &str) {
1038-
loop {
1039-
let pending = {
1040-
let mut states = self.run_input_states.lock().await;
1041-
let Some(state) = states.get_mut(run_id) else {
1042-
return;
1043-
};
1044-
if state.outstanding_tool_uses > 0 {
1045-
return;
1046-
}
1047-
state.queued_inputs.pop_front()
1048-
};
1049-
1050-
let Some(pending) = pending else {
1051-
return;
1052-
};
1053-
1054-
let sent = self
1055-
.send_streaming_input_now(run_id, &pending.pending_input_id, pending.outbound)
1056-
.await;
1057-
if !sent {
1058-
return;
1059-
}
1060-
}
1061-
}
1062-
1063901
async fn initialize_pending_inputs(&self, run_id: &str) {
1064902
self.run_pending_inputs.lock().await.insert(
1065903
run_id.to_owned(),
@@ -1349,7 +1187,6 @@ impl ClaudeCliProvider {
13491187
Ok(None) => break, // stream closed normally
13501188
Ok(Some(msg_result)) => match msg_result {
13511189
Ok(Message::User(user_msg)) => {
1352-
let tool_result_count = count_user_tool_result_messages(&user_msg);
13531190
if let claude_agent_sdk::UserContent::Blocks(ref blocks) = user_msg.content
13541191
{
13551192
extract_tool_session_messages(
@@ -1358,12 +1195,10 @@ impl ClaudeCliProvider {
13581195
Some(on_chunk),
13591196
user_msg.parent_tool_use_id.as_deref(),
13601197
);
1361-
}
1362-
if tool_result_count > 0 {
1363-
self.note_tool_results_finished(run_id, tool_result_count)
1364-
.await;
1365-
self.flush_queued_streaming_inputs_if_safe(run_id).await;
1366-
continue;
1198+
if has_tool_result_blocks(blocks) || user_msg.tool_use_result.is_some()
1199+
{
1200+
continue;
1201+
}
13671202
}
13681203
// Keep Python parity: normal UserMessage echoes are ACKs that
13691204
// indicate one queued user input has been consumed.
@@ -1380,18 +1215,13 @@ impl ClaudeCliProvider {
13801215
Ok(Message::Assistant(assistant_msg)) => {
13811216
if is_synthetic_no_response_message(&assistant_msg) {
13821217
tracing::debug!(
1383-
run_id = %run_id,
1384-
thread_id = %thread_id,
1218+
run_id = %run_id,
1219+
thread_id = %thread_id,
13851220
"suppressing claude synthetic no-response placeholder"
13861221
);
13871222
continue;
13881223
}
13891224
assistant_or_tool_activity_seen = true;
1390-
self.note_tool_uses_started(
1391-
run_id,
1392-
count_tool_use_blocks(&assistant_msg.content),
1393-
)
1394-
.await;
13951225
if actual_model.is_none() {
13961226
actual_model = Some(assistant_msg.model.trim().to_owned())
13971227
.filter(|value| !value.is_empty());
@@ -1435,7 +1265,6 @@ impl ClaudeCliProvider {
14351265
thread_title: thread_title.clone(),
14361266
session_messages: session_messages.clone(),
14371267
});
1438-
self.flush_queued_streaming_inputs_if_safe(run_id).await;
14391268
// Atomically check-and-close: if no queued inputs remain,
14401269
// remove the queue entry so that concurrent
14411270
// add_streaming_input callers see the run as closed and
@@ -1617,7 +1446,6 @@ impl AgentLoopProvider for ClaudeCliProvider {
16171446
};
16181447
self.run_session_map.lock().await.clear();
16191448
self.run_pending_inputs.lock().await.clear();
1620-
self.run_input_states.lock().await.clear();
16211449

16221450
for (run_id, run) in runs {
16231451
if let Err(e) = run.interrupt().await {
@@ -1793,8 +1621,8 @@ impl AgentLoopProvider for ClaudeCliProvider {
17931621

17941622
async fn add_streaming_input(&self, thread_id: &str, input: QueuedUserInput) -> bool {
17951623
if let Some(run_id) = self.resolve_active_run_id_for_session(thread_id).await {
1796-
let has_run = { self.active_runs.lock().await.contains_key(&run_id) };
1797-
if has_run {
1624+
let run = { self.active_runs.lock().await.get(&run_id).cloned() };
1625+
if let Some(run) = run {
17981626
let pending_input_id = input.pending_input_id.unwrap_or_default();
17991627
if pending_input_id.trim().is_empty() {
18001628
return false;
@@ -1813,14 +1641,21 @@ impl AgentLoopProvider for ClaudeCliProvider {
18131641
UserInput::Text(text) => OutboundUserMessage::text(text, ""),
18141642
UserInput::Blocks(blocks) => OutboundUserMessage::blocks(blocks, ""),
18151643
};
1816-
if self
1817-
.send_or_queue_streaming_input(&run_id, pending_input_id.clone(), outbound)
1818-
.await
1819-
{
1820-
tracing::debug!(thread_id = %thread_id, "queued streaming input");
1821-
true
1822-
} else {
1823-
false
1644+
match run.send_user_message(outbound).await {
1645+
Ok(()) => {
1646+
tracing::debug!(thread_id = %thread_id, "queued streaming input");
1647+
true
1648+
}
1649+
Err(e) => {
1650+
self.rollback_pending_input(&run_id, &pending_input_id)
1651+
.await;
1652+
tracing::warn!(
1653+
thread_id = %thread_id,
1654+
error = %e,
1655+
"failed to queue streaming input"
1656+
);
1657+
false
1658+
}
18241659
}
18251660
} else {
18261661
let _ = self.unregister_run(&run_id).await;

0 commit comments

Comments
 (0)