diff --git a/server/src/db/mod.rs b/server/src/db/mod.rs index d1c005c7..cbfe1479 100644 --- a/server/src/db/mod.rs +++ b/server/src/db/mod.rs @@ -18,13 +18,17 @@ pub mod content_store; /// Write operations for the pipeline — content, stage transitions, and deletions. pub mod ops; /// Recovery for half-written pipeline items (bug 1001 backfill). +/// +/// No public MCP surface — kept as an in-process library for future incident +/// response. Re-expose via diagnostics::tool_recover_half_written_items if +/// the bug ever resurfaces. +#[allow(dead_code)] pub mod recover; /// Background shadow-write task — persists pipeline items to SQLite asynchronously. pub mod shadow_write; pub use content_store::{ContentKey, all_content_ids, delete_content, read_content, write_content}; pub use ops::{ItemMeta, delete_item, move_item_stage, next_item_number, write_item_with_content}; -pub use recover::{find_half_written_items, recover_half_written_items}; pub use shadow_write::init; #[cfg(test)] diff --git a/server/src/http/mcp/diagnostics/mod.rs b/server/src/http/mcp/diagnostics/mod.rs index cb251eb6..ad013c38 100644 --- a/server/src/http/mcp/diagnostics/mod.rs +++ b/server/src/http/mcp/diagnostics/mod.rs @@ -115,62 +115,6 @@ pub(crate) fn tool_dump_crdt(args: &Value) -> Result { .map_err(|e| format!("Serialization error: {e}")) } -/// MCP tool: discover or recover half-written pipeline items (bug 1001). -/// -/// A half-written item is one whose content row is in the content store / -/// SQLite shadow but whose CRDT entry is absent or tombstoned. Such items -/// are invisible to every CRDT-driven read path and to `delete_story` / -/// `purge_story`, so they need explicit recovery. -/// -/// With `dry_run = true` (default), returns the list of discovered half- -/// writes without mutating anything. With `dry_run = false`, lifts each -/// orphan onto a fresh non-tombstoned id and returns the old→new mapping. -pub(crate) fn tool_recover_half_written_items(args: &Value) -> Result { - let dry_run = args - .get("dry_run") - .and_then(|v| v.as_bool()) - .unwrap_or(true); - - // Optional id filter — when provided, recovery (or the dry-run report) is - // restricted to these ids. This is the safe choice for live systems - // where the orphan set may include many historic purged stories that - // should stay dead. - let only: Option> = args.get("story_ids").and_then(|v| { - v.as_array().map(|arr| { - arr.iter() - .filter_map(|x| x.as_str().map(str::to_string)) - .collect() - }) - }); - - if dry_run { - let mut half = crate::db::find_half_written_items(); - if let Some(filter) = &only { - half.retain(|h| filter.iter().any(|f| f == &h.story_id)); - } - let count = half.len(); - return serde_json::to_string_pretty(&json!({ - "dry_run": true, - "found": half, - "count": count, - "message": format!( - "Discovered {count} half-written item(s){scope}. Re-run with dry_run=false to recover them.", - scope = if only.is_some() { " matching the filter" } else { "" }, - ), - })) - .map_err(|e| format!("Serialization error: {e}")); - } - - let results = crate::db::recover_half_written_items(only.as_deref()); - serde_json::to_string_pretty(&json!({ - "dry_run": false, - "recovered": results, - "count": results.len(), - "message": format!("Recovered {} half-written item(s).", results.len()), - })) - .map_err(|e| format!("Serialization error: {e}")) -} - /// MCP tool: return the server version, build hash, and running port. pub(crate) fn tool_get_version(ctx: &AppContext) -> Result { let build_hash = diff --git a/server/src/http/mcp/dispatch.rs b/server/src/http/mcp/dispatch.rs index 62bb1e73..ad5cecf6 100644 --- a/server/src/http/mcp/dispatch.rs +++ b/server/src/http/mcp/dispatch.rs @@ -93,8 +93,6 @@ pub async fn dispatch_tool_call( "purge_story" => story_tools::tool_purge_story(&args, ctx), // Debug CRDT dump (story 515) "dump_crdt" => diagnostics::tool_dump_crdt(&args), - // Recover half-written pipeline items (bug 1001) - "recover_half_written_items" => diagnostics::tool_recover_half_written_items(&args), // Read-only peer mesh diagnostics (story 720) "mesh_status" => diagnostics::tool_mesh_status(&args), // Arbitrary pipeline movement diff --git a/server/src/http/mcp/tools_list/mod.rs b/server/src/http/mcp/tools_list/mod.rs index 971492a9..3234c9ab 100644 --- a/server/src/http/mcp/tools_list/mod.rs +++ b/server/src/http/mcp/tools_list/mod.rs @@ -96,7 +96,6 @@ mod tests { assert!(names.contains(&"status")); assert!(names.contains(&"loc_file")); assert!(names.contains(&"dump_crdt")); - assert!(names.contains(&"recover_half_written_items")); assert!(names.contains(&"get_version")); assert!(names.contains(&"remove_criterion")); assert!(names.contains(&"mesh_status")); @@ -107,7 +106,7 @@ mod tests { assert!(names.contains(&"show_epic")); assert!(names.contains(&"freeze_story")); assert!(names.contains(&"unfreeze_story")); - assert_eq!(tools.len(), 75); + assert_eq!(tools.len(), 74); } #[test] diff --git a/server/src/http/mcp/tools_list/system_tools.rs b/server/src/http/mcp/tools_list/system_tools.rs index 1d02b8dc..7e0f09d5 100644 --- a/server/src/http/mcp/tools_list/system_tools.rs +++ b/server/src/http/mcp/tools_list/system_tools.rs @@ -288,25 +288,6 @@ pub(super) fn system_tools() -> Vec { "required": [] } }), - json!({ - "name": "recover_half_written_items", - "description": "Discover (and optionally recover) half-written pipeline items — rows whose content is in the SQLite shadow and content store but whose CRDT entry is absent or tombstoned. These are invisible to every CRDT-driven read path and can't be cleaned up by delete_story / purge_story. Recovery moves each orphan's content onto a fresh non-tombstoned id and reports the old→new mapping. Defaults to dry_run=true so you can see what would change before mutating. Use story_ids to restrict the operation to a specific list of ids — strongly recommended on live systems where the orphan set may include historic purged stories that should stay dead.", - "inputSchema": { - "type": "object", - "properties": { - "dry_run": { - "type": "boolean", - "description": "When true (default), only discover half-written items and report them. When false, perform the recovery." - }, - "story_ids": { - "type": "array", - "items": { "type": "string" }, - "description": "Optional: restrict the operation (discovery in dry-run, recovery otherwise) to these story_ids. Anything else in the orphan set is left untouched. Strongly recommended for live recovery so you don't resurrect historic purged items." - } - }, - "required": [] - } - }), json!({ "name": "wizard_status", "description": "Return the current setup wizard state: which step is active, and which are done/skipped/pending. Use this to inspect progress before calling wizard_generate, wizard_confirm, wizard_skip, or wizard_retry.",