mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-09 22:33:37 +00:00
Add duration_ms, node_visits, and base_branch to shadow branch structs
Enrich Outcome with stage duration, Checkpoint with persisted node visit counts (avoiding fragile reconstruction on resume), and Manifest with the base branch name for offline analysis without git lookups. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e24d20b439
commit
96bcff05b3
7 changed files with 44 additions and 3 deletions
|
|
@ -33,6 +33,9 @@ pub struct Checkpoint {
|
|||
/// Failure signature counts across loop_restart edges.
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub restart_failure_signatures: HashMap<FailureSignature, usize>,
|
||||
/// Per-node visit counts persisted for accurate resume.
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub node_visits: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
impl Checkpoint {
|
||||
|
|
@ -47,6 +50,7 @@ impl Checkpoint {
|
|||
next_node_id: Option<String>,
|
||||
loop_failure_signatures: HashMap<FailureSignature, usize>,
|
||||
restart_failure_signatures: HashMap<FailureSignature, usize>,
|
||||
node_visits: HashMap<String, usize>,
|
||||
) -> Self {
|
||||
Self {
|
||||
timestamp: Utc::now(),
|
||||
|
|
@ -60,6 +64,7 @@ impl Checkpoint {
|
|||
git_commit_sha: None,
|
||||
loop_failure_signatures,
|
||||
restart_failure_signatures,
|
||||
node_visits,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +108,7 @@ mod tests {
|
|||
None,
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
);
|
||||
|
||||
assert_eq!(cp.current_node, "node_a");
|
||||
|
|
@ -142,6 +148,7 @@ mod tests {
|
|||
Some("next_step".to_string()),
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
);
|
||||
|
||||
cp.save(&path).unwrap();
|
||||
|
|
@ -190,6 +197,7 @@ mod tests {
|
|||
None,
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
HashMap::new(),
|
||||
);
|
||||
|
||||
let json = serde_json::to_string(&cp).unwrap();
|
||||
|
|
@ -230,6 +238,7 @@ mod tests {
|
|||
None,
|
||||
loop_sigs,
|
||||
restart_sigs,
|
||||
HashMap::new(),
|
||||
);
|
||||
cp.save(&path).unwrap();
|
||||
|
||||
|
|
|
|||
|
|
@ -303,6 +303,7 @@ fn write_manifest(
|
|||
run_branch: config.run_branch.clone(),
|
||||
base_sha: config.base_sha.clone(),
|
||||
labels: config.labels.clone(),
|
||||
base_branch: config.base_branch.clone(),
|
||||
};
|
||||
let _ = std::fs::create_dir_all(logs_root);
|
||||
let _ = manifest.save(&logs_root.join("manifest.json"));
|
||||
|
|
@ -1328,9 +1329,13 @@ impl WorkflowRunEngine {
|
|||
context.append_log(log_entry.clone());
|
||||
}
|
||||
completed_nodes = cp.completed_nodes.clone();
|
||||
// Rebuild visit counts from completed_nodes (which records every visit)
|
||||
for id in &completed_nodes {
|
||||
*loop_state.node_visits.entry(id.clone()).or_insert(0) += 1;
|
||||
// Use persisted node_visits; fall back to reconstruction for old checkpoints
|
||||
if cp.node_visits.is_empty() {
|
||||
for id in &completed_nodes {
|
||||
*loop_state.node_visits.entry(id.clone()).or_insert(0) += 1;
|
||||
}
|
||||
} else {
|
||||
loop_state.node_visits = cp.node_visits.clone();
|
||||
}
|
||||
// Gap #5: Restore retry counters from checkpoint
|
||||
node_retries = cp.node_retries.clone();
|
||||
|
|
@ -1781,6 +1786,7 @@ impl WorkflowRunEngine {
|
|||
}
|
||||
|
||||
// Step 3: Record completion
|
||||
outcome.duration_ms = Some(stage_duration_ms);
|
||||
completed_nodes.push(node.id.clone());
|
||||
node_outcomes.insert(node.id.clone(), outcome.clone());
|
||||
previous_node_id = Some(node.id.clone());
|
||||
|
|
@ -1881,6 +1887,7 @@ impl WorkflowRunEngine {
|
|||
next_node_id_for_checkpoint,
|
||||
loop_state.loop_failure_signatures.clone(),
|
||||
loop_state.restart_failure_signatures.clone(),
|
||||
loop_state.node_visits.clone(),
|
||||
);
|
||||
let checkpoint_path = config.logs_root.join("checkpoint.json");
|
||||
if let Err(e) = checkpoint.save(&checkpoint_path) {
|
||||
|
|
|
|||
|
|
@ -850,6 +850,7 @@ mod tests {
|
|||
Some("node_b".to_string()),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
);
|
||||
let cp_json = serde_json::to_vec_pretty(&cp).unwrap();
|
||||
store.write_checkpoint("RUN2", &cp_json, &[]).unwrap();
|
||||
|
|
@ -884,6 +885,7 @@ mod tests {
|
|||
None,
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
);
|
||||
let cp1_json = serde_json::to_vec_pretty(&cp1).unwrap();
|
||||
store.write_checkpoint("RUN3", &cp1_json, &[]).unwrap();
|
||||
|
|
@ -897,6 +899,7 @@ mod tests {
|
|||
Some("node_c".to_string()),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
);
|
||||
let cp2_json = serde_json::to_vec_pretty(&cp2).unwrap();
|
||||
store.write_checkpoint("RUN3", &cp2_json, &[]).unwrap();
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ pub struct Manifest {
|
|||
pub base_sha: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub labels: HashMap<String, String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_branch: Option<String>,
|
||||
}
|
||||
|
||||
impl Manifest {
|
||||
|
|
@ -47,6 +49,7 @@ mod tests {
|
|||
run_branch: Some("feature/test".to_string()),
|
||||
base_sha: Some("abc123".to_string()),
|
||||
labels: HashMap::from([("env".into(), "test".into())]),
|
||||
base_branch: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ pub struct Outcome {
|
|||
/// Used by the parallel handler to skip re-executing branch nodes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub jump_to_node: Option<String>,
|
||||
/// Wall-clock duration of the stage execution in milliseconds.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub duration_ms: Option<u64>,
|
||||
}
|
||||
|
||||
impl Outcome {
|
||||
|
|
@ -117,6 +120,7 @@ impl Outcome {
|
|||
usage: None,
|
||||
files_touched: Vec::new(),
|
||||
jump_to_node: None,
|
||||
duration_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +136,7 @@ impl Outcome {
|
|||
usage: None,
|
||||
files_touched: Vec::new(),
|
||||
jump_to_node: None,
|
||||
duration_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -149,6 +154,7 @@ impl Outcome {
|
|||
usage: None,
|
||||
files_touched: Vec::new(),
|
||||
jump_to_node: None,
|
||||
duration_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,6 +172,7 @@ impl Outcome {
|
|||
usage: None,
|
||||
files_touched: Vec::new(),
|
||||
jump_to_node: None,
|
||||
duration_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -190,6 +197,7 @@ impl Outcome {
|
|||
usage: None,
|
||||
files_touched: Vec::new(),
|
||||
jump_to_node: None,
|
||||
duration_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -350,6 +350,7 @@ mod tests {
|
|||
git_commit_sha: None,
|
||||
loop_failure_signatures: HashMap::new(),
|
||||
restart_failure_signatures: HashMap::new(),
|
||||
node_visits: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -410,6 +411,7 @@ mod tests {
|
|||
git_commit_sha: None,
|
||||
loop_failure_signatures: HashMap::new(),
|
||||
restart_failure_signatures: HashMap::new(),
|
||||
node_visits: HashMap::new(),
|
||||
};
|
||||
|
||||
let retro = derive_retro(
|
||||
|
|
|
|||
|
|
@ -1141,6 +1141,7 @@ fn checkpoint_save_and_resume_roundtrip() {
|
|||
None,
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
);
|
||||
|
||||
checkpoint.save(&path).expect("save should succeed");
|
||||
|
|
@ -1620,6 +1621,7 @@ async fn resume_from_checkpoint_completes_pipeline() {
|
|||
Some("step_b".to_string()),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
);
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
|
@ -1721,6 +1723,7 @@ async fn resume_from_checkpoint_preserves_goal_gate_outcomes() {
|
|||
Some("step_b".to_string()),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
);
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
|
@ -2672,6 +2675,7 @@ async fn scenario_crash_recovery() {
|
|||
Some("b".to_string()),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
);
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
|
@ -4678,6 +4682,7 @@ async fn fidelity_resume_degrades_full_to_summary_high() {
|
|||
Some("step_b".to_string()),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
);
|
||||
|
||||
let captures = FidelityCaptures::new();
|
||||
|
|
@ -4776,6 +4781,7 @@ async fn fidelity_resume_degrade_only_affects_first_hop() {
|
|||
Some("step_b".to_string()),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
);
|
||||
|
||||
let captures = FidelityCaptures::new();
|
||||
|
|
@ -4861,6 +4867,7 @@ async fn fidelity_resume_no_degrade_when_not_full() {
|
|||
Some("step_b".to_string()),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
);
|
||||
|
||||
let captures = FidelityCaptures::new();
|
||||
|
|
@ -5739,6 +5746,7 @@ async fn fidelity_resume_preserves_context_values_across_checkpoint() {
|
|||
Some("step_b".to_string()),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
std::collections::HashMap::new(),
|
||||
);
|
||||
|
||||
let captures = FidelityCaptures::new();
|
||||
|
|
@ -11903,6 +11911,7 @@ fn e2e_checkpoint_signatures_roundtrip() {
|
|||
None,
|
||||
loop_sigs,
|
||||
restart_sigs,
|
||||
std::collections::HashMap::new(),
|
||||
);
|
||||
cp.save(&path).unwrap();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue