98d496b1ad
Bug 901: `unblock_story` (and the chat `unblock` command) routed through
`parse_front_matter` and errored with "Missing front matter" on any
post-865 story (story content is now CRDT-only with no YAML on disk).
In `chat/commands/unblock.rs::unblock_by_story_id`:
- Drop the early `parse_front_matter` gate.
- Read story name and blocked state from the CRDT register API instead
of parsed YAML (`crdt_state::read_item`, `pipeline_state::read_typed`).
- Keep the legacy fallback cleanup, but gate it on the content actually
starting with a `---` YAML block, so CRDT-only stories don't hit a
parse error there either.
- Remove the now-unused `parse_front_matter` import.
Surfaced a second sub-bug: even when the state-machine transition
fired (`Blocked + Unblock → Coding`), the CRDT `blocked` register was
never explicitly cleared. Pre-865 the YAML-strip content_transform
cleared it as a side effect; post-865 there is no YAML to strip.
- Add `crdt_state::set_blocked(story_id, bool)` parallel to
`set_retry_count`. Wired through `crdt_state::write` and the
crate-level re-export.
- `agents::lifecycle::transition_to_unblocked` now calls
`set_blocked(story_id, false)` alongside `set_retry_count(0)` so
the legacy register stays in sync with the typed stage.
Test: `unblock_command_works_on_crdt_only_story_no_yaml` seeds a CRDT
entry with no YAML on disk, runs unblock, asserts success + cleared
blocked + retry_count=0. All 10 existing unblock tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
72 lines
3.0 KiB
Rust
72 lines
3.0 KiB
Rust
//! CRDT state layer — manages pipeline state as a conflict-free replicated document backed by SQLite.
|
|
//!
|
|
//! The CRDT document is the primary source of truth for pipeline item
|
|
//! metadata (stage, name, agent, etc.). CRDT ops are persisted to SQLite so
|
|
//! state survives restarts. The filesystem `.huskies/work/` directories are
|
|
//! still updated as a secondary output for backwards compatibility.
|
|
//!
|
|
//! Stage transitions detected by `write_item()` are broadcast as [`CrdtEvent`]s
|
|
//! so subscribers (auto-assign, WebSocket, notifications) can react without
|
|
//! polling the filesystem.
|
|
|
|
#![allow(unused_imports, dead_code)]
|
|
use std::collections::HashMap;
|
|
|
|
/// A vector clock mapping node IDs (hex-encoded Ed25519 pubkeys) to the count
|
|
/// of ops seen from that node. Used for delta sync — a connecting peer sends
|
|
/// its clock so the other side can compute which ops are missing.
|
|
pub type VectorClock = HashMap<String, u64>;
|
|
|
|
mod gateway_config;
|
|
mod lww_maps;
|
|
mod ops;
|
|
mod presence;
|
|
mod read;
|
|
mod state;
|
|
mod types;
|
|
mod write;
|
|
|
|
pub use gateway_config::{read_gateway_active_project, write_gateway_active_project};
|
|
pub use lww_maps::{
|
|
delete_active_agent, delete_agent_throttle, delete_gateway_project, delete_merge_job,
|
|
delete_test_job, delete_token_usage, read_active_agent, read_agent_throttle,
|
|
read_all_active_agents, read_all_agent_throttles, read_all_gateway_projects,
|
|
read_all_merge_jobs, read_all_test_jobs, read_all_token_usage, read_gateway_project,
|
|
read_merge_job, read_test_job, read_token_usage, write_active_agent, write_agent_throttle,
|
|
write_gateway_project, write_merge_job, write_test_job, write_token_usage,
|
|
};
|
|
pub use ops::{all_ops_json, apply_remote_op, ops_since, our_vector_clock, subscribe_ops};
|
|
pub use presence::{
|
|
is_claimed_by_us, our_node_id, read_all_node_presence, release_claim, sign_challenge,
|
|
sign_versioned_challenge, write_claim, write_node_metadata, write_node_presence,
|
|
};
|
|
pub use read::{
|
|
CrdtItemDump, CrdtStateDump, check_archived_deps_crdt, check_unmet_deps_crdt,
|
|
dep_is_archived_crdt, dep_is_done_crdt, dump_crdt_state, evict_item, read_all_items, read_item,
|
|
};
|
|
pub use state::init;
|
|
pub use types::{
|
|
ActiveAgentCrdt, ActiveAgentView, AgentThrottleCrdt, AgentThrottleView, CrdtEvent,
|
|
GatewayConfigCrdt, GatewayProjectCrdt, GatewayProjectView, MergeJobCrdt, MergeJobView,
|
|
NodePresenceCrdt, NodePresenceView, PipelineDoc, PipelineItemCrdt, PipelineItemView,
|
|
TestJobCrdt, TestJobView, TokenUsageCrdt, TokenUsageView, subscribe,
|
|
};
|
|
pub use write::{
|
|
bump_retry_count, migrate_names_from_slugs, migrate_story_ids_to_numeric, name_from_story_id,
|
|
set_agent, set_blocked, set_depends_on, set_mergemaster_attempted, set_qa_mode,
|
|
set_retry_count, write_item,
|
|
};
|
|
|
|
#[cfg(test)]
|
|
pub use state::init_for_test;
|
|
|
|
pub(crate) use state::{ALL_OPS, VECTOR_CLOCK};
|
|
|
|
/// Hex-encode a byte slice (no external dep needed).
|
|
pub(crate) mod hex {
|
|
/// Encode `bytes` as a lowercase hexadecimal string.
|
|
pub fn encode(bytes: &[u8]) -> String {
|
|
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
|
}
|
|
}
|