Skip to content

Commit a708ef4

Browse files
Pyinerclaude
andcommitted
Gate ACK-confirmed submit on no active SDK MCP runtime; release v0.2.6
v0.2.5's ACK-confirmed submit re-pasted the prompt when no transcript ACK appeared, disrupting SDK MCP control-channel negotiation (a second initialize) on stream-json turns using sdkMcpServers, regressing 4 integration tests. Gate the ACK + clear-and-resubmit path on having no active SDK MCP runtime; SDK MCP turns keep the plain paste+enter submit. Gateway cctty mode (garyx http MCP, no SDK MCP) still uses ACK to survive the post-trust-dialog transitional screen. cargo test green (lib 56 / cctty_cli 36). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 67c8996 commit a708ef4

3 files changed

Lines changed: 49 additions & 9 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "cctty"
3-
version = "0.2.5"
3+
version = "0.2.6"
44
edition = "2024"
55
description = "Claude Agent SDK compatibility through the interactive Claude Code TTY"
66
license = "MIT"

src/runner.rs

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,7 +1025,7 @@ async fn submit_prompt_and_tail(
10251025
include_partial_messages: bool,
10261026
) -> Result<TranscriptState> {
10271027
tail.prepare_offset()?;
1028-
submit_prompt_to_tty(process, tail, prompt).await?;
1028+
submit_prompt_to_tty(process, tail, prompt, true).await?;
10291029
tail_until_complete(process, tail, output_format, include_partial_messages).await
10301030
}
10311031

@@ -1040,7 +1040,13 @@ async fn submit_prompt_and_tail_stream(
10401040
include_partial_messages: bool,
10411041
) -> Result<TranscriptState> {
10421042
tail.prepare_offset()?;
1043-
submit_prompt_to_tty(process, tail, prompt).await?;
1043+
submit_prompt_to_tty(
1044+
process,
1045+
tail,
1046+
prompt,
1047+
sdk_state.sdk_mcp_server_names().is_empty(),
1048+
)
1049+
.await?;
10441050
tail_until_complete_stream(
10451051
process,
10461052
tail,
@@ -1057,11 +1063,29 @@ async fn submit_prompt_to_tty(
10571063
process: &mut PtyProcess,
10581064
tail: &mut TailCursor,
10591065
prompt: &str,
1066+
confirm_via_ack: bool,
10601067
) -> Result<()> {
10611068
logging::event(format!(
10621069
"prompt_submit start content_chars={}",
10631070
prompt.chars().count()
10641071
));
1072+
if !confirm_via_ack {
1073+
// Plain submit: paste, and press Enter only if the prompt is still
1074+
// visibly being edited. Used when an SDK MCP runtime is active — its
1075+
// control-channel negotiation (initialize/tools-list/tools-call) would be
1076+
// disrupted by the ACK path's clear-and-re-paste.
1077+
process.write_all(&bracketed_paste_input(prompt))?;
1078+
tokio::time::sleep(Duration::from_millis(120)).await;
1079+
maybe_log_submit_tty_diagnostic(process, "after_paste");
1080+
if tty_output_still_editing_prompt(&process.recent_output(), prompt) {
1081+
logging::event("prompt_submit_retry reason=prompt_still_visible");
1082+
process.write_all(b"\r")?;
1083+
tokio::time::sleep(Duration::from_millis(120)).await;
1084+
maybe_log_submit_tty_diagnostic(process, "after_retry_enter");
1085+
}
1086+
logging::event("prompt_submit done");
1087+
return Ok(());
1088+
}
10651089
// Confirm the submission via the transcript (an ACK that Claude actually
10661090
// accepted the message) instead of trusting the on-screen state. When Claude
10671091
// is slow to become input-ready — connecting MCP servers, or right after the
@@ -1096,7 +1120,7 @@ async fn submit_prompt_to_tty(
10961120
tokio::time::sleep(Duration::from_millis(120)).await;
10971121
maybe_log_submit_tty_diagnostic(process, "after_retry_enter");
10981122
}
1099-
if wait_for_transcript_ack(tail, ACK_TIMEOUT).await? {
1123+
if wait_for_transcript_ack(process, tail, ACK_TIMEOUT).await? {
11001124
logging::event(format!("prompt_submit done attempt={attempt} ack=true"));
11011125
return Ok(());
11021126
}
@@ -1105,11 +1129,22 @@ async fn submit_prompt_to_tty(
11051129
Ok(())
11061130
}
11071131

1108-
/// Wait until the target transcript shows new bytes — Claude's acknowledgement
1109-
/// that it accepted the submitted prompt — or the timeout elapses.
1110-
async fn wait_for_transcript_ack(tail: &mut TailCursor, timeout: Duration) -> Result<bool> {
1132+
/// Confirm Claude accepted the submitted prompt. Returns true as soon as the
1133+
/// transcript shows new bytes, or Claude is clearly producing (busy) / has
1134+
/// finished a turn that leaves no transcript (cctty synthesizes it downstream).
1135+
/// Returns false only while Claude sits at a clean, idle prompt with no
1136+
/// transcript — i.e. the paste didn't land and the caller should re-submit. This
1137+
/// reuses the same completion heuristics as the tail loop so fake-Claude /
1138+
/// no-transcript turns aren't mistaken for a dropped paste.
1139+
async fn wait_for_transcript_ack(
1140+
process: &mut PtyProcess,
1141+
tail: &mut TailCursor,
1142+
timeout: Duration,
1143+
) -> Result<bool> {
11111144
let deadline = Instant::now() + timeout;
1145+
let mut progress = TtyVisibleProgress::new(process);
11121146
loop {
1147+
progress.observe(process);
11131148
if let Some(path) = tail.resolve_path()? {
11141149
if std::fs::metadata(&path)
11151150
.map(|meta| meta.len() > tail.offset)
@@ -1118,10 +1153,15 @@ async fn wait_for_transcript_ack(tail: &mut TailCursor, timeout: Duration) -> Re
11181153
return Ok(true);
11191154
}
11201155
}
1156+
if tty_wait_class(&process.recent_output()) != "prompt_ready"
1157+
|| progress.completed_without_transcript(process)
1158+
{
1159+
return Ok(true);
1160+
}
11211161
if Instant::now() >= deadline {
11221162
return Ok(false);
11231163
}
1124-
tokio::time::sleep(Duration::from_millis(250)).await;
1164+
tokio::time::sleep(Duration::from_millis(200)).await;
11251165
}
11261166
}
11271167

0 commit comments

Comments
 (0)