huskies: merge 490_story_crdt_state_layer_backed_by_sqlite
CRDT state layer backed by SQLite for pipeline state. Integrates the BFT JSON CRDT crate with SQLite persistence via sqlx. Ops are persisted and replayed on startup. Node identity via Ed25519 keypair. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -72,8 +72,62 @@ pub struct PipelineState {
|
||||
}
|
||||
|
||||
/// Load the full pipeline state (all 5 active stages).
|
||||
///
|
||||
/// Reads from the CRDT document when available, falling back to the
|
||||
/// filesystem for any items not yet in the CRDT (e.g. first run before
|
||||
/// migration). Agent assignments are always overlaid from the in-memory
|
||||
/// agent pool.
|
||||
pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
|
||||
let agent_map = build_active_agent_map(ctx);
|
||||
|
||||
// Try CRDT-first read.
|
||||
if let Some(crdt_items) = crate::crdt_state::read_all_items() {
|
||||
let mut state = PipelineState {
|
||||
backlog: Vec::new(),
|
||||
current: Vec::new(),
|
||||
qa: Vec::new(),
|
||||
merge: Vec::new(),
|
||||
done: Vec::new(),
|
||||
};
|
||||
|
||||
for item in crdt_items {
|
||||
let agent = agent_map.get(&item.story_id).cloned();
|
||||
let story = UpcomingStory {
|
||||
story_id: item.story_id,
|
||||
name: item.name,
|
||||
error: None,
|
||||
merge_failure: None,
|
||||
agent,
|
||||
review_hold: None,
|
||||
qa: None,
|
||||
retry_count: item.retry_count.map(|r| r as u32),
|
||||
blocked: item.blocked,
|
||||
depends_on: item.depends_on,
|
||||
};
|
||||
match item.stage.as_str() {
|
||||
"1_backlog" => state.backlog.push(story),
|
||||
"2_current" => state.current.push(story),
|
||||
"3_qa" => state.qa.push(story),
|
||||
"4_merge" => state.merge.push(story),
|
||||
"5_done" => state.done.push(story),
|
||||
_ => {} // ignore archived or unknown stages
|
||||
}
|
||||
}
|
||||
|
||||
// Sort each stage for deterministic output.
|
||||
state.backlog.sort_by(|a, b| a.story_id.cmp(&b.story_id));
|
||||
state.current.sort_by(|a, b| a.story_id.cmp(&b.story_id));
|
||||
state.qa.sort_by(|a, b| a.story_id.cmp(&b.story_id));
|
||||
state.merge.sort_by(|a, b| a.story_id.cmp(&b.story_id));
|
||||
state.done.sort_by(|a, b| a.story_id.cmp(&b.story_id));
|
||||
|
||||
// Merge in any filesystem-only items not yet in the CRDT.
|
||||
merge_filesystem_items(ctx, &mut state, &agent_map)?;
|
||||
|
||||
return Ok(state);
|
||||
}
|
||||
|
||||
// Fallback: filesystem-only read (CRDT not initialised).
|
||||
Ok(PipelineState {
|
||||
backlog: load_stage_items(ctx, "1_backlog", &HashMap::new())?,
|
||||
current: load_stage_items(ctx, "2_current", &agent_map)?,
|
||||
@@ -83,6 +137,38 @@ pub fn load_pipeline_state(ctx: &AppContext) -> Result<PipelineState, String> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Merge filesystem items that are not already present in the CRDT state.
|
||||
fn merge_filesystem_items(
|
||||
ctx: &AppContext,
|
||||
state: &mut PipelineState,
|
||||
agent_map: &HashMap<String, AgentAssignment>,
|
||||
) -> Result<(), String> {
|
||||
let stages = [
|
||||
("1_backlog", &mut state.backlog),
|
||||
("2_current", &mut state.current),
|
||||
("3_qa", &mut state.qa),
|
||||
("4_merge", &mut state.merge),
|
||||
("5_done", &mut state.done),
|
||||
];
|
||||
|
||||
for (stage_dir, stage_vec) in stages {
|
||||
let empty_map = HashMap::new();
|
||||
let map = if stage_dir == "2_current" || stage_dir == "3_qa" || stage_dir == "4_merge" {
|
||||
agent_map
|
||||
} else {
|
||||
&empty_map
|
||||
};
|
||||
let fs_items = load_stage_items(ctx, stage_dir, map)?;
|
||||
for fs_item in fs_items {
|
||||
if !stage_vec.iter().any(|s| s.story_id == fs_item.story_id) {
|
||||
stage_vec.push(fs_item);
|
||||
}
|
||||
}
|
||||
stage_vec.sort_by(|a, b| a.story_id.cmp(&b.story_id));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build a map from story_id → AgentAssignment for all pending/running agents.
|
||||
fn build_active_agent_map(ctx: &AppContext) -> HashMap<String, AgentAssignment> {
|
||||
let agents = match ctx.agents.list_agents() {
|
||||
|
||||
Reference in New Issue
Block a user