From 8bf54d2a0fbfc96cc52a82f9cc553640ec14de5e Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:01:49 +0800 Subject: [PATCH] Revert "feat(core): send a Claude turn's context size once its transcript catches up" This reverts commit 3544bc6b172901977749c6d6041650ae3795be37. --- crates/tty7-core/src/core/agent_hooks.rs | 199 +---------------------- crates/tty7-core/src/core/cli_agent.rs | 9 - crates/tty7-core/src/daemon/pane.rs | 11 -- 3 files changed, 5 insertions(+), 214 deletions(-) diff --git a/crates/tty7-core/src/core/agent_hooks.rs b/crates/tty7-core/src/core/agent_hooks.rs index 094e526b..083a1881 100644 --- a/crates/tty7-core/src/core/agent_hooks.rs +++ b/crates/tty7-core/src/core/agent_hooks.rs @@ -21,96 +21,10 @@ pub fn run_agent_hook(agent: &str, event: &str) { if !std::io::stdin().is_terminal() { let _ = std::io::stdin().take(MAX_STDIN).read_to_string(&mut input); } - if agent == "claude" && event == LATE_READOUT { - if let Some(seq) = late_readout(&input) { - write_to_controlling_tty(&seq); - } - return; - } let Some(event) = effective_event(agent, event, &input) else { return; }; write_to_controlling_tty(&build_hook_sequence(agent, event, &input)); - if agent == "claude" && event == "stop" && !reply_is_written(&input) { - spawn_late_readout(&input); - } -} - -/// The hook's own follow-up to a Claude `stop`, run as a second process. -const LATE_READOUT: &str = "readout"; - -/// How long the follow-up waits for the turn's reply to reach the transcript. -const LATE_READOUT_WAIT: std::time::Duration = std::time::Duration::from_secs(3); - -/// Whether the transcript already holds the reply a `stop` payload names. -/// Claude writes the transcript on its own schedule, and at `stop` the reply -/// that ended the turn is usually not in it yet. -fn reply_is_written(stdin_json: &str) -> bool { - let Ok(payload) = serde_json::from_str::(stdin_json) else { - return true; - }; - let Some(path) = payload.get("transcript_path").and_then(|p| p.as_str()) else { - return true; - }; - let Some(said) = payload - .get("last_assistant_message") - .and_then(|m| m.as_str()) - .filter(|m| !m.trim().is_empty()) - else { - return true; - }; - last_assistant_reply(Path::new(path)).is_some_and(|reply| reply.says(said)) -} - -/// Hand the wait to a process of its own, so the agent is not held up by -/// it. Its output goes nowhere: an inherited pipe would keep the agent -/// waiting on this hook until the follow-up is done. -fn spawn_late_readout(stdin_json: &str) { - use std::io::Write as _; - use std::process::{Command, Stdio}; - let Ok(exe) = std::env::current_exe() else { - return; - }; - let Ok(mut child) = Command::new(exe) - .args(["agent-hook", "claude", LATE_READOUT]) - .stdin(Stdio::piped()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .spawn() - else { - return; - }; - if let Some(mut stdin) = child.stdin.take() { - let _ = stdin.write_all(stdin_json.as_bytes()); - } -} - -/// The follow-up: wait for the reply to reach the transcript, then send what -/// it says about the context as a readout of its own. -fn late_readout(stdin_json: &str) -> Option> { - let payload: serde_json::Value = serde_json::from_str(stdin_json).ok()?; - let path = PathBuf::from(payload.get("transcript_path")?.as_str()?); - let said = payload - .get("last_assistant_message") - .and_then(|m| m.as_str()) - .unwrap_or_default() - .to_string(); - let deadline = std::time::Instant::now() + LATE_READOUT_WAIT; - loop { - if last_assistant_reply(&path).is_some_and(|reply| reply.says(&said)) { - break; - } - if std::time::Instant::now() >= deadline { - return None; - } - std::thread::sleep(std::time::Duration::from_millis(100)); - } - let mut body = serde_json::json!({ "v": 1, "agent": "claude", "event": LATE_READOUT }); - if let Some(id) = payload.get("session_id").and_then(|v| v.as_str()) { - body["session_id"] = id.into(); - } - claude_readout(&payload, &mut body); - Some(format!("\x1b]777;notify;{AGENT_EVENT_SENTINEL};{body}\x07").into_bytes()) } #[cfg(not(unix))] @@ -342,30 +256,10 @@ fn claude_settings() -> serde_json::Value { .unwrap_or(serde_json::json!({})) } -/// The last reply in a transcript: its model, how much context it used (its -/// input, cache and output tokens together, which is what the next request -/// carries), and its text. -struct Reply { - model: String, - tokens: u64, - text: String, -} - -impl Reply { - /// Whether this is the reply a hook payload quotes. The payload may clip - /// it, and the transcript may split it, so either may hold the other. - fn says(&self, said: &str) -> bool { - let (text, said) = (self.text.trim(), said.trim()); - !text.is_empty() && !said.is_empty() && (text.contains(said) || said.contains(text)) - } -} - -fn last_assistant_usage(path: &Path) -> Option<(String, u64)> { - last_assistant_reply(path).map(|r| (r.model, r.tokens)) -} - +/// The model and context size of the last reply in a transcript: its input, +/// cache and output tokens together, which is what the next request carries. /// Subagents' entries are skipped — they run in contexts of their own. -fn last_assistant_reply(path: &Path) -> Option { +fn last_assistant_usage(path: &Path) -> Option<(String, u64)> { use std::io::{Seek as _, SeekFrom}; let mut file = std::fs::File::open(path).ok()?; let len = file.metadata().ok()?.len(); @@ -373,14 +267,10 @@ fn last_assistant_reply(path: &Path) -> Option { .ok()?; let mut tail = Vec::new(); file.read_to_end(&mut tail).ok()?; - last_reply_in(&String::from_utf8_lossy(&tail)) + last_usage_in(&String::from_utf8_lossy(&tail)) } fn last_usage_in(jsonl: &str) -> Option<(String, u64)> { - last_reply_in(jsonl).map(|r| (r.model, r.tokens)) -} - -fn last_reply_in(jsonl: &str) -> Option { jsonl.lines().rev().find_map(|line| { let entry: serde_json::Value = serde_json::from_str(line).ok()?; if entry.get("type")?.as_str()? != "assistant" @@ -396,24 +286,8 @@ fn last_reply_in(jsonl: &str) -> Option { + n("cache_read_input_tokens") + n("output_tokens"); let model = message.get("model")?.as_str()?.to_string(); - let text = message - .get("content") - .and_then(|c| c.as_array()) - .map(|blocks| { - blocks - .iter() - .filter(|b| b.get("type").and_then(|t| t.as_str()) == Some("text")) - .filter_map(|b| b.get("text").and_then(|t| t.as_str())) - .collect::>() - .join("\n") - }) - .unwrap_or_default(); // Claude Code writes its own error notices as assistant entries. - (model != "").then_some(Reply { - model, - tokens, - text, - }) + (model != "").then_some((model, tokens)) }) } @@ -2283,69 +2157,6 @@ mod tests { assert_eq!(configured_effort(&serde_json::json!({}), None), None); } - #[test] - fn a_stop_the_transcript_has_not_caught_up_with_gets_its_readout_late() { - let dir = std::env::temp_dir().join(format!("tty7-late-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); - let transcript = dir.join("t.jsonl"); - let reply = |text: &str, tokens: u64| { - serde_json::json!({ - "type": "assistant", - "message": { - "model": "claude-sonnet-5-5", - "content": [{ "type": "text", "text": text }], - "usage": { "input_tokens": tokens }, - }, - }) - .to_string() - }; - std::fs::write(&transcript, reply("An earlier answer.", 1000) + "\n").unwrap(); - let stdin = serde_json::json!({ - "session_id": "s", - "transcript_path": transcript, - "last_assistant_message": "Hi! What would you like to work on?", - }) - .to_string(); - assert!( - !reply_is_written(&stdin), - "the transcript still ends on the turn before" - ); - assert_eq!( - late_readout_now(&stdin), - None, - "nothing to send until the reply lands" - ); - - std::fs::write( - &transcript, - reply("An earlier answer.", 1000) - + "\n" - + &reply("Hi! What would you like to work on?", 58_849) - + "\n", - ) - .unwrap(); - assert!(reply_is_written(&stdin)); - let seq = late_readout(&stdin).expect("the reply is there now"); - let _ = std::fs::remove_dir_all(&dir); - let ev = crate::core::cli_agent::parse_agent_event( - seq.strip_prefix(b"\x1b]") - .unwrap() - .strip_suffix(b"\x07") - .unwrap(), - ) - .unwrap(); - assert_eq!(ev.kind, crate::core::cli_agent::AgentEventKind::Readout); - assert_eq!(ev.readout.context_tokens, Some(58_849)); - assert_eq!(ev.session_id.as_deref(), Some("s")); - } - - /// `late_readout` without its wait, for the case that must not send. - fn late_readout_now(stdin: &str) -> Option> { - reply_is_written(stdin) - .then(|| late_readout(stdin)) - .flatten() - } - #[test] fn other_agents_carry_no_readout() { let stdin = r#"{"session_id":"s","permission_mode":"plan"}"#; diff --git a/crates/tty7-core/src/core/cli_agent.rs b/crates/tty7-core/src/core/cli_agent.rs index 0214ad88..2764f1d6 100644 --- a/crates/tty7-core/src/core/cli_agent.rs +++ b/crates/tty7-core/src/core/cli_agent.rs @@ -1129,10 +1129,6 @@ impl AgentSessionState { } pub fn apply_event(&mut self, ev: &AgentEvent) { - if ev.kind == AgentEventKind::Readout { - self.readout.merge(&ev.readout); - return; - } self.rich = true; let guessed = std::mem::take(&mut self.inferred); if let Some(id) = &ev.session_id { @@ -1180,7 +1176,6 @@ impl AgentSessionState { self.message = None; self.cwd = None; } - AgentEventKind::Readout => {} } } } @@ -1196,10 +1191,6 @@ pub enum AgentEventKind { Notification, Stop, SessionEnd, - /// Only the readout, sent after the fact: Claude writes its transcript - /// after the hooks of the turn that produced it have run, so the context - /// size of a finished turn comes in a moment behind its `Stop`. - Readout, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/crates/tty7-core/src/daemon/pane.rs b/crates/tty7-core/src/daemon/pane.rs index a1ca4b20..ac9f36f0 100644 --- a/crates/tty7-core/src/daemon/pane.rs +++ b/crates/tty7-core/src/daemon/pane.rs @@ -3446,17 +3446,6 @@ fn apply_agent_signals( { continue; } - // A readout comes a moment behind the turn it describes, by which time - // the agent may have quit. It says nothing about whether one is - // running, nor is it activity: it only fills in the session there is. - if event.kind == crate::core::cli_agent::AgentEventKind::Readout { - if st.agent.is_some() - && let Some(sess) = &mut st.agent_session - { - sess.apply_event(event); - } - continue; - } if st.agent.is_none() && event.agent.is_some() { st.agent = event.agent; notify(st, DaemonMsg::Agent(st.agent));