huskies: merge 1052

This commit is contained in:
dave
2026-05-14 18:04:35 +00:00
parent 977b954e98
commit b9709a6466
6 changed files with 73 additions and 5 deletions
+2 -2
View File
@@ -55,8 +55,8 @@ pub use types::{
pub use write::{
bump_retry_count, migrate_legacy_stage_strings, migrate_merge_job, migrate_names_from_slugs,
migrate_node_claims_to_agent_claims, migrate_story_ids_to_numeric, name_from_story_id,
set_agent, set_depends_on, set_epic, set_item_type, set_name, set_plan_state, set_qa_mode,
set_resume_to, set_resume_to_raw, set_retry_count, write_item,
purge_done_stage_merge_jobs, set_agent, set_depends_on, set_epic, set_item_type, set_name,
set_plan_state, set_qa_mode, set_resume_to, set_resume_to_raw, set_retry_count, write_item,
};
#[cfg(test)]
+55
View File
@@ -650,6 +650,61 @@ pub fn migrate_merge_job(db_path: &std::path::Path) {
slog!("[crdt] Migrated {count} MergeJob entries to typed MergeResult format");
}
/// Delete MergeJob CRDT entries for stories that have reached a terminal stage
/// (Done, Archived, Abandoned, Superseded, Rejected).
///
/// Pre-1036 code left stale MergeJob entries after a story recovered from a
/// merge failure and transitioned to Done. Those entries caused the gateway
/// UI to mislabel recovered stories as "FAILED" because `load_pipeline_state`
/// unconditionally read the MergeJob error field for every story. This
/// migration removes the orphaned entries so the state is clean on the next
/// server start.
///
/// Running this migration repeatedly is safe — tombstoned entries are filtered
/// out by the read path, so subsequent calls are no-ops.
pub fn purge_done_stage_merge_jobs() {
use crate::pipeline_state::Stage;
let Some(jobs) = crate::crdt_state::read_all_merge_jobs() else {
return;
};
if jobs.is_empty() {
return;
}
// Collect IDs of every story currently in a terminal stage.
let terminal_ids: std::collections::HashSet<String> = crate::pipeline_state::read_all_typed()
.into_iter()
.filter(|item| {
matches!(
item.stage,
Stage::Done { .. }
| Stage::Archived { .. }
| Stage::Abandoned { .. }
| Stage::Superseded { .. }
| Stage::Rejected { .. }
)
})
.map(|item| item.story_id.0.clone())
.collect();
let to_delete: Vec<String> = jobs
.into_iter()
.filter(|j| terminal_ids.contains(&j.story_id))
.map(|j| j.story_id)
.collect();
if to_delete.is_empty() {
return;
}
let count = to_delete.len();
for story_id in &to_delete {
crate::crdt_state::delete_merge_job(story_id);
}
slog!("[crdt] Purged {count} stale MergeJob entries for terminal-stage stories");
}
#[cfg(test)]
mod merge_job_migration_tests {
use super::super::super::state::init_for_test;
+1
View File
@@ -19,4 +19,5 @@ pub use item::write_item_str;
pub use migrations::{
migrate_legacy_stage_strings, migrate_merge_job, migrate_names_from_slugs,
migrate_node_claims_to_agent_claims, migrate_story_ids_to_numeric, name_from_story_id,
purge_done_stage_merge_jobs,
};
+11 -2
View File
@@ -116,7 +116,9 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
// Story 945: review_hold is `Stage::ReviewHold`; qa_mode and epic_id
// come from typed CRDT registers. merge_failure detail lives on the
// MergeJob CRDT entry (same as status_tools.rs).
// MergeJob CRDT entry, but only surfaces for items currently in a
// failure stage — Done/terminal items must never inherit stale errors
// from a previously-failed merge attempt (story 1052).
let view = crate::crdt_state::read_item(sid);
let review_hold = if matches!(item.stage, Stage::ReviewHold { .. }) {
Some(true)
@@ -125,7 +127,14 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
};
let qa = view.as_ref().and_then(|v| v.qa_mode());
let epic_id = view.as_ref().and_then(|v| v.epic());
let merge_failure = crate::crdt_state::read_merge_job(sid).and_then(|j| j.error);
let merge_failure = if matches!(
item.stage,
Stage::MergeFailure { .. } | Stage::MergeFailureFinal { .. }
) {
crate::crdt_state::read_merge_job(sid).and_then(|j| j.error)
} else {
None
};
let story = UpcomingStory {
story_id: sid.clone(),
+1 -1
View File
@@ -47,7 +47,7 @@ pub fn aggregate_pipeline_counts(pipeline: &Value) -> Value {
.map(|f| !f.is_null() && f != "")
.unwrap_or(false);
if is_blocked || has_merge_failure {
if (is_blocked || has_merge_failure) && stage != "done" {
let story_id = item
.get("story_id")
.and_then(|s| s.as_str())
+3
View File
@@ -167,6 +167,9 @@ pub(crate) async fn init_subsystems(app_state: &Arc<SessionState>, cwd: &Path) {
crdt_state::migrate_merge_job(db_path);
// Story 1009: drop legacy node-hex claims that can't be converted to AgentName.
crdt_state::migrate_node_claims_to_agent_claims();
// Story 1052: remove stale MergeJob entries for terminal-stage
// stories so they can never cause "FAILED" labels in the UI.
crdt_state::purge_done_stage_merge_jobs();
}
}
}