Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
25ab074
acpmux: test that an adopted chat replays its tool calls (failing)
teamleaderleo Oct 19, 2026
132d96d
acpmux: a reopened Codex or Claude chat shows each turn's tool calls
teamleaderleo Oct 19, 2026
3a9e3b6
acpmux: cargo fmt
teamleaderleo Oct 19, 2026
5725a85
chore(next): PR autofix at 2c2b9a6e90ea
Oct 19, 2026
68edd1d
acpmux: a failed Codex patch replays no edits, a replayed hunk is no …
teamleaderleo Oct 20, 2026
cae2336
cmux-next: test the edited-files card, Changes menu entry and summary…
teamleaderleo Oct 19, 2026
6e7f72b
cmux-next: list Changes in the chat menu and the edited files under O…
teamleaderleo Oct 19, 2026
f87fcbf
cmux-next: Outputs keeps the files only the turn's checkpoint knows, …
teamleaderleo Oct 20, 2026
f043fcf
Merge feat-cmux-next into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
6bd22c3
Merge fix/next-adopted-tool-calls (with feat-cmux-next) into fix/next…
teamleaderleo Oct 20, 2026
7888811
Merge origin/feat-cmux-next into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
5451569
cmux-next: an unconfirmed Claude edit shows no diff; a file the last …
teamleaderleo Oct 20, 2026
980bcb2
Merge origin/fix/next-edited-files-card
teamleaderleo Oct 20, 2026
924fc9f
acpmux: adopt replay tests need no backdated transcript after #22139;…
teamleaderleo Oct 20, 2026
129646a
Merge fix/next-adopted-tool-calls into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
d3496ae
cmux-next: Outputs matches a last-turn file to an output by exact pat…
teamleaderleo Oct 20, 2026
1d3da29
style: vp fmt
teamleaderleo Oct 20, 2026
36c1a90
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
50a6897
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
102a637
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
68b794e
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
9efe5e0
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
0ec87e1
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
de20ead
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
c8a67f7
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
bf0401b
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
5895912
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
7495327
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
d20bc3d
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
22803e3
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
522cd15
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
3502dc6
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
d4cba0b
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
0dcc75a
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
d15d42f
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
f7030d2
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
f4be3e9
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
86fcda5
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
84cb978
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
04a635f
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
cb74002
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
401cb3a
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
58ded39
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
940bc1c
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
cef5c89
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
25ddbbc
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
a512056
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
36b41e1
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
e0e2a15
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
be722a9
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
8bb9e7e
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
c607f9c
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
ce9e46d
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
29d301e
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
f59ca18
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
e777b9f
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
ae0f896
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
265960a
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
4be6ec3
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
0638d2e
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
6fd6d5e
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
587dba8
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
c428a61
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
2a01c94
Merge branch 'feat-cmux-next' into fix/next-adopted-tool-calls
teamleaderleo Oct 20, 2026
8431509
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 20, 2026
ea0ba51
Merge remote-tracking branch 'origin/feat-cmux-next' into fix/next-ad…
teamleaderleo Oct 21, 2026
94b1359
Merge branch 'fix/next-adopted-tool-calls' into fix/next-edited-files…
teamleaderleo Oct 21, 2026
0e94a30
Merge branch 'feat-cmux-next' into fix/next-edited-files-card
teamleaderleo Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 77 additions & 25 deletions cmux-tui/crates/acpmux/src/adopt_replay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@
//! Adopting resumes a Claude Code or Codex conversation in a new acpmux session (`adopt.rs`).
//! Claude Code's `--resume` replays nothing to the client, so the pane showed an empty chat. This
//! reads the harness's own transcript (the file `adopt::find` found) into turns: each person
//! prompt and the assistant text that answered it. `Hub::replay_adopted` appends them as acpmux
//! history (`turn_started`, `user_message`, `agent_message_chunk`, `turn_end`) before the session
//! prompt, the tool calls the turn made (`adopt_tools.rs`) and the assistant text that answered
//! it. `Hub::replay_adopted` appends them as acpmux history (`turn_started`, `user_message`,
//! `tool_call` and `tool_call_update`, `agent_message_chunk`, `turn_end`) before the session
//! continues, the same records a live turn leaves, so every client draws them.

use std::io::{BufRead, BufReader, Seek, SeekFrom};
use std::path::Path;

use serde_json::Value;

/// One past exchange: the person's prompt and the assistant's answer text.
use crate::adopt_tools::{PastTool, ToolResult};

/// One past exchange: the person's prompt, the tool calls it led to, in order, and the
/// assistant's answer text.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PastTurn {
pub prompt: String,
pub tools: Vec<PastTool>,
pub answer: String,
/// When the prompt was sent and the answer last written (ms since the epoch), when recorded.
pub prompt_at: Option<u64>,
Expand All @@ -25,6 +30,8 @@ pub struct PastTurn {
/// At most this many turns (the newest) and this much text per answer: a long chat stays cheap.
const MAX_TURNS: usize = 200;
const MAX_ANSWER: usize = 20_000;
/// At most this many tool calls per turn (the first).
const MAX_TOOLS: usize = 400;
/// A transcript bigger than this is read for its newest part only.
const MAX_BYTES: u64 = 64 << 20;

Expand All @@ -45,6 +52,9 @@ pub fn read(family: &str, file: &Path) -> Vec<PastTurn> {
// Current Codex writes no event messages: from its first annotated item on (a chat begun on
// an older Codex and resumed on a current one), items are the turns.
let mut current = false;
// Current Codex records each finished tool call as an `item_completed` event; older Codex
// wrote `exec_command_end` and patch events, read until the first such item.
let mut tool_items = false;
for (index, line) in BufReader::new(f).lines().map_while(Result::ok).enumerate() {
if skip_first && index == 0 {
continue;
Expand All @@ -54,14 +64,22 @@ pub fn read(family: &str, file: &Path) -> Vec<PastTurn> {
match family {
"codex" => {
current = current || has_item_kinds(&record);
if let Some(step) = codex_step(&record) {
if let Some((tool, item)) = crate::adopt_tools::codex_tool(&record, at) {
tool_items = tool_items || item;
if item || !tool_items {
// With the turns that are being read: event messages, else items.
let into =
if current || !turns.is_empty() { &mut turns } else { &mut items };
add(into, tool.map_or_else(Step::ToolResult, Step::Tool), at);
}
} else if let Some(step) = codex_step(&record) {
add(&mut turns, step, at);
} else if let Some(step) = codex_item_step(&record) {
add(if current { &mut turns } else { &mut items }, step, at);
}
}
_ => {
if let Some(step) = claude_step(&record) {
for step in claude_steps(&record, at) {
add(&mut turns, step, at);
}
}
Expand All @@ -78,17 +96,35 @@ pub fn read(family: &str, file: &Path) -> Vec<PastTurn> {
enum Step {
Prompt(String),
Answer(String),
Tool(PastTool),
ToolResult(ToolResult),
}

/// A prompt starts a turn; an answer adds to the last one, capped at MAX_ANSWER.
/// A prompt starts a turn; an answer adds to the last one, capped at MAX_ANSWER; a tool call
/// joins the last one (once: a call recorded again replaces it), and a result settles its call.
fn add(turns: &mut Vec<PastTurn>, step: Step, at: Option<u64>) {
match step {
Step::Prompt(text) => turns.push(PastTurn {
prompt: text,
tools: Vec::new(),
answer: String::new(),
prompt_at: at,
answer_at: at,
}),
Step::Tool(tool) => {
let Some(turn) = turns.last_mut() else { return };
if let Some(known) = turn.tools.iter_mut().find(|t| t.id == tool.id) {
*known = tool;
} else if turn.tools.len() < MAX_TOOLS {
turn.tools.push(tool);
}
}
Step::ToolResult(result) => {
let Some(turn) = turns.last_mut() else { return };
if let Some(tool) = turn.tools.iter_mut().rev().find(|t| t.id == result.id) {
tool.settle(&result, at);
}
}
Step::Answer(text) => {
let Some(turn) = turns.last_mut() else { return };
turn.answer_at = at.or(turn.answer_at);
Expand All @@ -108,34 +144,50 @@ fn add(turns: &mut Vec<PastTurn>, step: Step, at: Option<u64>) {
}

/// Claude Code: `{"type":"user","message":{"content":...}}` (a typed prompt: a string or text
/// blocks; tool results and meta lines are not prompts) and `{"type":"assistant",...}` text blocks.
fn claude_step(record: &Value) -> Option<Step> {
/// blocks; tool results settle their calls, and meta lines are not prompts) and
/// `{"type":"assistant",...}` text and `tool_use` blocks.
fn claude_steps(record: &Value, at: Option<u64>) -> Vec<Step> {
let flag = |key: &str| record.get(key).and_then(Value::as_bool) == Some(true);
if flag("isSidechain") || flag("isMeta") || flag("isCompactSummary") || !from_person(record) {
return None;
return Vec::new();
}
let content = record.pointer("/message/content")?;
match record.get("type").and_then(Value::as_str)? {
"user" => {
// A tool result is not a prompt, also beside text (as `hub::fork_at::is_prompt`).
let Some(content) = record.pointer("/message/content") else { return Vec::new() };
match record.get("type").and_then(Value::as_str) {
// A tool result is not a prompt, also beside text (as `hub::fork_at::is_prompt`).
Some("user")
if content.as_array().is_some_and(|blocks| {
blocks.iter().any(|b| b.get("type").and_then(Value::as_str) == Some("tool_result"))
}) {
return None;
}
let text = text_of(content)?;
let start = text.trim_start();
// Command and hook echoes and interruption markers are not prompts.
let echo = ["<command-", "<local-command-", "<bash-", "[Request interrupted"]
.iter()
.any(|prefix| start.starts_with(prefix));
(!echo).then_some(Step::Prompt(text))
}) =>
{
crate::adopt_tools::claude_tool_results(content)
.into_iter()
.map(Step::ToolResult)
.collect()
}
"assistant" => text_of(content).map(Step::Answer),
_ => None,
Some("assistant") => {
let mut steps: Vec<Step> = crate::adopt_tools::claude_tool_uses(content, at)
.into_iter()
.map(Step::Tool)
.collect();
steps.extend(text_of(content).map(Step::Answer));
steps
}
Some("user") => claude_prompt(content).into_iter().collect(),
_ => Vec::new(),
}
}

/// A Claude Code prompt: a user message's text, unless it echoes a command or a hook or marks
/// an interruption.
fn claude_prompt(content: &Value) -> Option<Step> {
let text = text_of(content)?;
let start = text.trim_start();
let echo = ["<command-", "<local-command-", "<bash-", "[Request interrupted"]
.iter()
.any(|prefix| start.starts_with(prefix));
(!echo).then_some(Step::Prompt(text))
}

/// Whether a Claude Code line came from the person: it has no `origin`, or its `origin.kind` is
/// `human`. Claude Code also writes user lines itself (`task-notification` when a background task
/// or subagent finishes, `peer` for another session's `<cross-session-message>`).
Expand Down
Loading