mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-12 18:18:10 -10:00
Compare commits
2
Commits
main
...
ci/cw-8146
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f00cab707 | ||
|
|
7e5bc98613 |
No files matched your search
@@ -9476,12 +9476,15 @@ enum SpanWriter {
|
||||
/// Split a window's changes into the turn's own and everyone else's, from
|
||||
/// the spans its receipts bound: a path belongs to the turn only if it
|
||||
/// changed while one of the turn's tool calls was running and, for a file
|
||||
/// tool, is one the call declared. A span whose changes were not recorded
|
||||
/// tool, is one the call declared. A path a sub-agent the turn started
|
||||
/// reported writing through a bounded write tool is the turn's own wherever
|
||||
/// it changed in the window. A span whose changes were not recorded
|
||||
/// (a snapshot in it failed) cannot be attributed and fails closed.
|
||||
fn attribute_window(
|
||||
workspace: &FsPath,
|
||||
turn_id: &str,
|
||||
receipts: &[codewhale_snapshot::WorkspaceSnapshotRef],
|
||||
delegated: &std::collections::BTreeSet<PathBuf>,
|
||||
) -> Result<
|
||||
(
|
||||
std::collections::BTreeSet<PathBuf>,
|
||||
@@ -9559,6 +9562,11 @@ fn attribute_window(
|
||||
}
|
||||
};
|
||||
}
|
||||
for path in delegated {
|
||||
if foreign.remove(path) {
|
||||
owned.insert(path.clone());
|
||||
}
|
||||
}
|
||||
Ok((owned, foreign))
|
||||
}
|
||||
|
||||
@@ -9733,7 +9741,12 @@ fn patch_undo_workspace_files_impl(
|
||||
})
|
||||
.filter_map(|receipt| receipt.write_paths.as_ref())
|
||||
.flatten();
|
||||
for raw in turn.declared_writes.iter().chain(receipt_writes) {
|
||||
for raw in turn
|
||||
.declared_writes
|
||||
.iter()
|
||||
.chain(&turn.delegated_writes)
|
||||
.chain(receipt_writes)
|
||||
{
|
||||
let covered = match declared_write_path(workspace, raw) {
|
||||
Some(rel) => !repo.path_is_excluded(&rel).map_err(|e| {
|
||||
ApiError::internal(format!(
|
||||
@@ -9759,7 +9772,12 @@ fn patch_undo_workspace_files_impl(
|
||||
for (turn, receipts) in windows {
|
||||
let (pre, pre_label) = resolve(&turn.turn_id, &receipts[0])?;
|
||||
let (post, _) = resolve(&turn.turn_id, &receipts[receipts.len() - 1])?;
|
||||
let (owned, foreign) = attribute_window(workspace, &turn.turn_id, receipts)?;
|
||||
let delegated = turn
|
||||
.delegated_writes
|
||||
.iter()
|
||||
.filter_map(|raw| declared_write_path(workspace, raw))
|
||||
.collect();
|
||||
let (owned, foreign) = attribute_window(workspace, &turn.turn_id, receipts, &delegated)?;
|
||||
segments.push(UndoSegment {
|
||||
turn_id: turn.turn_id.clone(),
|
||||
pre,
|
||||
|
||||
@@ -619,6 +619,7 @@ fn messages_from_thread_detail_batches_tool_results() {
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
};
|
||||
let item = |id: &str,
|
||||
kind: TurnItemKind,
|
||||
@@ -8155,6 +8156,7 @@ fn dropped_turn(
|
||||
may_change_files,
|
||||
snapshots,
|
||||
declared_writes: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
unrun_tool_calls: std::collections::BTreeSet::new(),
|
||||
}
|
||||
}
|
||||
@@ -8281,6 +8283,76 @@ fn patch_undo_helper_refuses_to_overwrite_later_edits() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Files a turn's own sub-agents wrote land while no lead tool call is open.
|
||||
/// They are the turn's own and the undo restores them; a file an editor
|
||||
/// changed in the same window stays foreign and still refuses the undo,
|
||||
/// naming only that file.
|
||||
#[test]
|
||||
fn patch_undo_helper_counts_sub_agent_writes_as_the_turns_own() -> Result<()> {
|
||||
use codewhale_snapshot::WorkspaceSnapshotKind::{PostTurn, PreTurn};
|
||||
if git_missing() {
|
||||
return Ok(());
|
||||
}
|
||||
let _lock = lock_test_env();
|
||||
let root = tempfile::tempdir()?;
|
||||
let home = root.path().join("home");
|
||||
fs::create_dir_all(&home)?;
|
||||
let _home = EnvVarGuard::set("HOME", &home);
|
||||
|
||||
let workspace = root.path().join("workspace");
|
||||
fs::create_dir_all(&workspace)?;
|
||||
let repo = codewhale_snapshot::SnapshotRepo::open_or_init(&workspace)?;
|
||||
let a = workspace.join("a.txt");
|
||||
let b = workspace.join("b.txt");
|
||||
let editor = workspace.join("editor.txt");
|
||||
fs::write(&a, "a before")?;
|
||||
fs::write(&b, "b before")?;
|
||||
fs::write(&editor, "editor before")?;
|
||||
let pre = repo.take_snapshot("pre-turn:1", Some("thr_a"))?;
|
||||
fs::write(&a, "a child")?;
|
||||
fs::write(&b, "b child")?;
|
||||
fs::write(&editor, "editor typed")?;
|
||||
let post = repo.take_snapshot("post-turn:1", Some("thr_a"))?;
|
||||
|
||||
let receipts = || {
|
||||
let pre_receipt = snapshot_receipt(PreTurn, &pre, "thr_a");
|
||||
let mut post_receipt = snapshot_receipt(PostTurn, &post, "thr_a");
|
||||
post_receipt.changed_paths = Some(changed_between(&repo, &pre, &post));
|
||||
vec![pre_receipt, post_receipt]
|
||||
};
|
||||
let mut turn = dropped_turn("turn_1", true, receipts());
|
||||
let err = patch_undo_workspace_files(&workspace, std::slice::from_ref(&turn))
|
||||
.expect_err("child writes without a receipt are foreign");
|
||||
assert_eq!(err.code, Some(PATCH_UNDO_WORKSPACE_CHANGED));
|
||||
|
||||
turn.delegated_writes = vec!["a.txt".to_string(), "b.txt".to_string()];
|
||||
let err = patch_undo_workspace_files(&workspace, std::slice::from_ref(&turn))
|
||||
.expect_err("the editor's file must still refuse the undo");
|
||||
assert_eq!(err.code, Some(PATCH_UNDO_WORKSPACE_CHANGED));
|
||||
assert!(err.message.contains("editor.txt"), "{}", err.message);
|
||||
assert!(!err.message.contains("a.txt"), "{}", err.message);
|
||||
assert!(!err.message.contains("b.txt"), "{}", err.message);
|
||||
assert_eq!(fs::read_to_string(&a)?, "a child");
|
||||
assert_eq!(fs::read_to_string(&editor)?, "editor typed");
|
||||
|
||||
fs::write(&editor, "editor before")?;
|
||||
let post = repo.take_snapshot("post-turn:1", Some("thr_a"))?;
|
||||
let mut post_receipt = snapshot_receipt(PostTurn, &post, "thr_a");
|
||||
post_receipt.changed_paths = Some(changed_between(&repo, &pre, &post));
|
||||
let turn = codewhale_engine::runtime_threads::manager::DroppedTurnSnapshots {
|
||||
snapshots: vec![snapshot_receipt(PreTurn, &pre, "thr_a"), post_receipt],
|
||||
delegated_writes: vec!["a.txt".to_string(), "b.txt".to_string()],
|
||||
..turn
|
||||
};
|
||||
let restored = patch_undo_workspace_files(&workspace, std::slice::from_ref(&turn))
|
||||
.expect("the turn's sub-agent writes are undone");
|
||||
assert!(restored.files_restored, "{:?}", restored.summary);
|
||||
assert_eq!(fs::read_to_string(&a)?, "a before");
|
||||
assert_eq!(fs::read_to_string(&b)?, "b before");
|
||||
assert_eq!(fs::read_to_string(&editor)?, "editor before");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// No recorded restore point, an unclosed window, or a pruned / retagged
|
||||
/// restore point: the undo fails honestly with a typed 409 instead of
|
||||
/// forking over changed files. A turn that ran no tools needs none.
|
||||
@@ -10646,6 +10718,7 @@ async fn session_save_merges_thread_cost_split_and_records_coverage() -> Result<
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
};
|
||||
// Mirror `TurnRecord::persist_effective_route` (private to the runtime
|
||||
// threads module): persist the route envelope onto the turn so the
|
||||
@@ -10858,6 +10931,7 @@ async fn session_save_persists_parent_cny_unpriced_reasons_without_double_count(
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
};
|
||||
turn.effective_provider = Some(ProviderKind::Openai.as_str().to_string());
|
||||
turn.effective_provider_id = Some(ProviderKind::Openai.as_str().to_string());
|
||||
@@ -12224,6 +12298,7 @@ fn seed_summary_search_transcript(
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
})?;
|
||||
latest_turn_id = Some(turn_id);
|
||||
}
|
||||
|
||||
@@ -1621,9 +1621,16 @@ async fn forward_subagent_mailbox_message(
|
||||
owner_session_id: &str,
|
||||
turn_id: &str,
|
||||
seq: u64,
|
||||
message: MailboxMessage,
|
||||
turn_seq: u64,
|
||||
mut message: MailboxMessage,
|
||||
best_effort_sent_at: &mut HashMap<String, Instant>,
|
||||
) -> bool {
|
||||
if let MailboxMessage::FileWrites {
|
||||
turn_seq: stamp, ..
|
||||
} = &mut message
|
||||
{
|
||||
*stamp = turn_seq;
|
||||
}
|
||||
let event = Event::SubAgentMailbox {
|
||||
owner_session_id: owner_session_id.to_string(),
|
||||
turn_id: turn_id.to_string(),
|
||||
@@ -6380,6 +6387,7 @@ impl Engine {
|
||||
let tx_event_clone = self.tx_event.clone();
|
||||
let mailbox_owner_session_id = self.session.id.clone();
|
||||
let mailbox_turn_id = turn_id.to_string();
|
||||
let mailbox_turn_seq = self.turn_counter;
|
||||
let (flush_tx, mut flush_rx) = tokio::sync::oneshot::channel();
|
||||
let drain_handle = spawn_supervised(
|
||||
"subagent-mailbox-drainer",
|
||||
@@ -6396,6 +6404,7 @@ impl Engine {
|
||||
&mailbox_owner_session_id,
|
||||
&mailbox_turn_id,
|
||||
envelope.seq,
|
||||
mailbox_turn_seq,
|
||||
envelope.message,
|
||||
&mut best_effort_sent_at,
|
||||
).await {
|
||||
@@ -6411,6 +6420,7 @@ impl Engine {
|
||||
&mailbox_owner_session_id,
|
||||
&mailbox_turn_id,
|
||||
envelope.seq,
|
||||
mailbox_turn_seq,
|
||||
envelope.message,
|
||||
&mut best_effort_sent_at,
|
||||
).await {
|
||||
|
||||
@@ -1141,6 +1141,11 @@ pub struct TurnRecord {
|
||||
/// carry none, and so have no restore point.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub workspace_snapshots: Vec<codewhale_snapshot::WorkspaceSnapshotRef>,
|
||||
/// Workspace-relative paths the sub-agents this turn started changed in
|
||||
/// the shared checkout through bounded write tools, as they reported
|
||||
/// them. A turn-scoped undo counts them as the turn's own changes.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub delegated_writes: Vec<String>,
|
||||
}
|
||||
|
||||
impl TurnRecord {
|
||||
@@ -1222,6 +1227,7 @@ impl TurnRecord {
|
||||
&& self.artifacts.is_empty()
|
||||
&& self.workspace.is_none()
|
||||
&& self.workspace_snapshots.is_empty()
|
||||
&& self.delegated_writes.is_empty()
|
||||
&& self.source_accounting
|
||||
&& self.source_usage_records.len() == 1
|
||||
&& self
|
||||
|
||||
@@ -1185,6 +1185,7 @@ fn unaccepted_usage_turn(thread_id: &str, turn_id: &str) -> TurnRecord {
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3050,6 +3051,10 @@ pub struct DroppedTurnSnapshots {
|
||||
/// only restore what the snapshots hold, so each is checked against the
|
||||
/// snapshot exclusions.
|
||||
pub declared_writes: Vec<String>,
|
||||
/// Paths the turn's own sub-agents reported changing through bounded
|
||||
/// write tools. They count as the turn's own changes, and are checked
|
||||
/// against the snapshot exclusions like `declared_writes`.
|
||||
pub delegated_writes: Vec<String>,
|
||||
/// Tool calls recorded as failed, canceled or never started: whatever
|
||||
/// paths their snapshot receipts declare, they wrote nothing.
|
||||
pub unrun_tool_calls: std::collections::BTreeSet<String>,
|
||||
@@ -10576,6 +10581,7 @@ impl RuntimeThreadManager {
|
||||
&& (ran_tools || !ran_here),
|
||||
snapshots: turn.workspace_snapshots.clone(),
|
||||
declared_writes,
|
||||
delegated_writes: turn.delegated_writes.clone(),
|
||||
unrun_tool_calls,
|
||||
}
|
||||
})
|
||||
@@ -11422,6 +11428,7 @@ impl RuntimeThreadManager {
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
});
|
||||
|
||||
thread.latest_turn_id = Some(turn_id);
|
||||
@@ -13712,6 +13719,7 @@ source_usage_records: Default::default(),
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
|
||||
};
|
||||
append_initial_routed_usage_to_turn(&mut turn, &initial_routed_usage);
|
||||
@@ -14308,6 +14316,7 @@ source_usage_records: Default::default(),
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
};
|
||||
let op = Op::CompactContext {
|
||||
id: compaction_id.clone(),
|
||||
@@ -16903,6 +16912,37 @@ source_usage_records: Default::default(),
|
||||
self.append_unverified_routed_usage_to_turn(&turn_id, &source_id, *route)?;
|
||||
}
|
||||
}
|
||||
EngineEvent::SubAgentMailbox {
|
||||
owner_session_id,
|
||||
turn_id: mailbox_turn_id,
|
||||
message: crate::tools::subagent::MailboxMessage::FileWrites { paths, .. },
|
||||
..
|
||||
} if owner_session_id == thread_id => {
|
||||
let belongs_to_turn = engine_turn_id
|
||||
.as_deref()
|
||||
.is_some_and(|started| started == mailbox_turn_id);
|
||||
if belongs_to_turn {
|
||||
let recorded = {
|
||||
let _turn_mutation = self.store.turn_mutation.lock();
|
||||
self.store.load_turn(&turn_id).and_then(|mut turn| {
|
||||
for path in paths {
|
||||
if !turn.delegated_writes.contains(&path) {
|
||||
turn.delegated_writes.push(path);
|
||||
}
|
||||
}
|
||||
self.store.save_turn(&turn)
|
||||
})
|
||||
};
|
||||
if let Err(err) = recorded {
|
||||
tracing::warn!(
|
||||
target: "snapshot",
|
||||
thread_id = %thread_id,
|
||||
turn_id = %turn_id,
|
||||
"sub-agent file writes were not recorded; undoing this turn will refuse them: {err:#}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
EngineEvent::CompactionStarted {
|
||||
id, auto, message, ..
|
||||
} => {
|
||||
|
||||
@@ -1081,6 +1081,7 @@ impl RuntimeThreadManager {
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
};
|
||||
let mut active = self.active.lock().await;
|
||||
let Some(state) = active.external.get_mut(&thread.id) else {
|
||||
|
||||
@@ -2719,6 +2719,7 @@ fn sample_turn(thread_id: &str, turn_id: &str, status: RuntimeTurnStatus) -> Tur
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20064,6 +20065,7 @@ fn opening_manager_recovers_stale_queued_and_in_progress_work() -> Result<()> {
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
})?;
|
||||
manager.store.save_turn(&TurnRecord {
|
||||
source_usage_origin_turn_id: None,
|
||||
@@ -20110,6 +20112,7 @@ fn opening_manager_recovers_stale_queued_and_in_progress_work() -> Result<()> {
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
})?;
|
||||
drop(manager);
|
||||
|
||||
@@ -20314,6 +20317,7 @@ fn seed_turns_with_user_messages(
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
})?;
|
||||
turn_ids.push(turn_id);
|
||||
}
|
||||
@@ -21079,6 +21083,7 @@ fn restart_rebuild_restores_tool_call_identity_from_persisted_items() -> Result<
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
})?;
|
||||
|
||||
let turns = manager.store.list_turns_for_thread(&thread.id)?;
|
||||
@@ -21372,6 +21377,7 @@ fn restart_rebuild_keeps_in_flight_tool_call_identity() -> Result<()> {
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
})?;
|
||||
|
||||
let turns = manager.store.list_turns_for_thread(&thread.id)?;
|
||||
@@ -21480,6 +21486,7 @@ fn restart_rebuild_skips_steers_the_engine_never_delivered() -> Result<()> {
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
})?;
|
||||
|
||||
let turns = manager.store.list_turns_for_thread(&thread.id)?;
|
||||
@@ -21589,6 +21596,7 @@ fn restart_rebuild_skips_legacy_tool_items_without_identity() -> Result<()> {
|
||||
artifacts: Vec::new(),
|
||||
workspace: None,
|
||||
workspace_snapshots: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
})?;
|
||||
|
||||
let turns = manager.store.list_turns_for_thread(&thread.id)?;
|
||||
|
||||
@@ -475,6 +475,20 @@ impl ChildAuthority {
|
||||
return;
|
||||
}
|
||||
let mut manager = self.coordination_manager.write().await;
|
||||
let shared_checkout = manager
|
||||
.worker_records
|
||||
.get(&self.owner_agent_id)
|
||||
.is_some_and(|record| {
|
||||
normalize_subagent_workspace(&record.spec.workspace)
|
||||
== normalize_subagent_workspace(&manager.workspace)
|
||||
});
|
||||
if shared_checkout && let Some(mailbox) = self.runtime.mailbox.as_ref() {
|
||||
let _ = mailbox.send(MailboxMessage::FileWrites {
|
||||
agent_id: self.owner_agent_id.clone(),
|
||||
turn_seq: 0,
|
||||
paths: paths.clone(),
|
||||
});
|
||||
}
|
||||
if let Some(record) = manager.worker_records.get_mut(&self.owner_agent_id) {
|
||||
record.delivery_evidence.observed_writes.extend(paths);
|
||||
}
|
||||
|
||||
@@ -76,6 +76,17 @@ pub enum MailboxMessage {
|
||||
#[serde(default)]
|
||||
todo: TodoListSnapshot,
|
||||
},
|
||||
/// Workspace files this agent's bounded write tools just changed in the
|
||||
/// shared checkout. `turn_seq` is the owning engine turn's sequence (the
|
||||
/// number in its `pre-turn:<seq>` restore point), stamped when the
|
||||
/// envelope is forwarded; a turn-scoped undo counts these paths as the
|
||||
/// turn's own.
|
||||
FileWrites {
|
||||
agent_id: String,
|
||||
#[serde(default)]
|
||||
turn_seq: u64,
|
||||
paths: Vec<String>,
|
||||
},
|
||||
/// Incremental token usage from a sub-agent's API call.
|
||||
/// Published after each turn so the parent's cost counter updates live.
|
||||
TokenUsage {
|
||||
@@ -104,6 +115,7 @@ impl MailboxMessage {
|
||||
| Self::Progress { agent_id, .. }
|
||||
| Self::ToolCallStarted { agent_id, .. }
|
||||
| Self::ToolCallCompleted { agent_id, .. }
|
||||
| Self::FileWrites { agent_id, .. }
|
||||
| Self::Completed { agent_id, .. }
|
||||
| Self::Failed { agent_id, .. }
|
||||
| Self::Interrupted { agent_id, .. }
|
||||
|
||||
@@ -300,6 +300,7 @@ pub(crate) struct UndoRangeInput {
|
||||
messages: std::sync::Arc<Vec<codewhale_models::Message>>,
|
||||
boundaries: Vec<usize>,
|
||||
first_request: usize,
|
||||
delegated_writes: std::collections::BTreeMap<u64, Vec<String>>,
|
||||
}
|
||||
|
||||
pub(crate) struct UndoRangePlan {
|
||||
@@ -352,6 +353,17 @@ pub(crate) fn undo_range_input(app: &App, cut: usize) -> anyhow::Result<UndoRang
|
||||
messages: app.api_messages.clone(),
|
||||
boundaries,
|
||||
first_request,
|
||||
delegated_writes: app
|
||||
.current_session_id
|
||||
.as_ref()
|
||||
.map(|session| {
|
||||
app.delegated_writes
|
||||
.iter()
|
||||
.filter(|((owner, _), _)| owner == session)
|
||||
.map(|((_, seq), paths)| (*seq, paths.iter().cloned().collect()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -394,6 +406,7 @@ pub(crate) fn plan_undo_range(input: UndoRangeInput) -> anyhow::Result<UndoRange
|
||||
may_change_files: false,
|
||||
snapshots: Vec::new(),
|
||||
declared_writes: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
unrun_tool_calls: Default::default(),
|
||||
},
|
||||
);
|
||||
@@ -460,6 +473,7 @@ pub(crate) fn plan_undo_range(input: UndoRangeInput) -> anyhow::Result<UndoRange
|
||||
may_change_files: false,
|
||||
snapshots: Vec::new(),
|
||||
declared_writes: Vec::new(),
|
||||
delegated_writes: Vec::new(),
|
||||
unrun_tool_calls: Default::default(),
|
||||
},
|
||||
);
|
||||
@@ -608,6 +622,11 @@ pub(crate) fn plan_undo_range(input: UndoRangeInput) -> anyhow::Result<UndoRange
|
||||
may_change_files: true,
|
||||
snapshots: receipts,
|
||||
declared_writes,
|
||||
delegated_writes: input
|
||||
.delegated_writes
|
||||
.get(&pre.seq.unwrap_or_default())
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
unrun_tool_calls: Default::default(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2089,6 +2089,11 @@ pub struct App {
|
||||
pub agent_progress: HashMap<String, String>,
|
||||
/// Parent/depth metadata for live progress-only sub-agent rows.
|
||||
pub agent_progress_meta: HashMap<String, AgentProgressMeta>,
|
||||
/// Workspace paths the sub-agents of each Engine turn reported writing,
|
||||
/// keyed by session id and the turn sequence in its `pre-turn:<seq>`
|
||||
/// restore point. Turn-scoped undo counts them as the turn's own changes.
|
||||
pub delegated_writes:
|
||||
std::collections::BTreeMap<(String, u64), std::collections::BTreeSet<String>>,
|
||||
/// In-transcript sub-agent card index by `agent_id` (issue #128).
|
||||
/// Maps each live sub-agent to the `HistoryCell::SubAgent` it renders
|
||||
/// into, so successive mailbox envelopes mutate the same cell rather
|
||||
|
||||
@@ -995,6 +995,7 @@ impl App {
|
||||
subagent_terminal_seen_at: HashMap::new(),
|
||||
agent_progress: HashMap::new(),
|
||||
agent_progress_meta: HashMap::new(),
|
||||
delegated_writes: Default::default(),
|
||||
subagent_card_index: HashMap::new(),
|
||||
last_fanout_card_index: None,
|
||||
pending_subagent_dispatch: None,
|
||||
|
||||
@@ -466,6 +466,19 @@ pub(super) fn handle_subagent_mailbox_for_turn(
|
||||
_seq: u64,
|
||||
message: &MailboxMessage,
|
||||
) -> bool {
|
||||
if let MailboxMessage::FileWrites {
|
||||
turn_seq, paths, ..
|
||||
} = message
|
||||
{
|
||||
if let Some(session) = app.current_session_id.clone() {
|
||||
app.delegated_writes
|
||||
.entry((session, *turn_seq))
|
||||
.or_default()
|
||||
.extend(paths.iter().cloned());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Accumulate sub-agent token costs for the real-time footer counter (#166).
|
||||
if let MailboxMessage::TokenUsage { record, .. } = message {
|
||||
if let Some(record) = record.as_ref().filter(|record| record.settlement.is_some()) {
|
||||
@@ -813,6 +826,7 @@ fn record_agent_current_activity(app: &mut App, message: &MailboxMessage) {
|
||||
previous.as_ref().and_then(|activity| activity.step),
|
||||
),
|
||||
MailboxMessage::TokenUsage { .. } => unreachable!("token usage handled above"),
|
||||
MailboxMessage::FileWrites { .. } => return,
|
||||
MailboxMessage::WorkState { .. } => unreachable!("work state handled above"),
|
||||
};
|
||||
|
||||
|
||||
@@ -663,7 +663,7 @@ pub fn apply_to_delegate(card: &mut DelegateCard, msg: &MailboxMessage) -> bool
|
||||
// to a sibling fanout card, not this one.
|
||||
return false;
|
||||
}
|
||||
MailboxMessage::TokenUsage { .. } => {
|
||||
MailboxMessage::TokenUsage { .. } | MailboxMessage::FileWrites { .. } => {
|
||||
// Cost accumulation happens in handle_subagent_mailbox (ui.rs)
|
||||
// before this apply function is called; TokenUsage never reaches
|
||||
// this arm in practice.
|
||||
@@ -720,6 +720,7 @@ pub fn apply_to_fanout(card: &mut FanoutCard, msg: &MailboxMessage) -> bool {
|
||||
// this arm in practice.
|
||||
true
|
||||
}
|
||||
MailboxMessage::FileWrites { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user