chore(1001): retire recover_half_written_items from MCP surface
The recovery tool was a one-shot migration aid for the half-written items that existed before the Stage 1 allocator fix. The three live orphans (989/1000/1001) have been migrated; the Stage 1 fix prevents new half-writes; the tool's job is done. Removes the MCP wrapper, schema, dispatch case, and tools-list assertion. The db::recover module itself stays in-process (under `#[allow(dead_code)]`) so it can be re-exposed quickly if the bug ever resurfaces — its regression tests still run as part of the default suite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,13 +18,17 @@ pub mod content_store;
|
|||||||
/// Write operations for the pipeline — content, stage transitions, and deletions.
|
/// Write operations for the pipeline — content, stage transitions, and deletions.
|
||||||
pub mod ops;
|
pub mod ops;
|
||||||
/// Recovery for half-written pipeline items (bug 1001 backfill).
|
/// 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;
|
pub mod recover;
|
||||||
/// Background shadow-write task — persists pipeline items to SQLite asynchronously.
|
/// Background shadow-write task — persists pipeline items to SQLite asynchronously.
|
||||||
pub mod shadow_write;
|
pub mod shadow_write;
|
||||||
|
|
||||||
pub use content_store::{ContentKey, all_content_ids, delete_content, read_content, write_content};
|
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 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;
|
pub use shadow_write::init;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -115,62 +115,6 @@ pub(crate) fn tool_dump_crdt(args: &Value) -> Result<String, String> {
|
|||||||
.map_err(|e| format!("Serialization error: {e}"))
|
.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<String, String> {
|
|
||||||
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<Vec<String>> = 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.
|
/// MCP tool: return the server version, build hash, and running port.
|
||||||
pub(crate) fn tool_get_version(ctx: &AppContext) -> Result<String, String> {
|
pub(crate) fn tool_get_version(ctx: &AppContext) -> Result<String, String> {
|
||||||
let build_hash =
|
let build_hash =
|
||||||
|
|||||||
@@ -93,8 +93,6 @@ pub async fn dispatch_tool_call(
|
|||||||
"purge_story" => story_tools::tool_purge_story(&args, ctx),
|
"purge_story" => story_tools::tool_purge_story(&args, ctx),
|
||||||
// Debug CRDT dump (story 515)
|
// Debug CRDT dump (story 515)
|
||||||
"dump_crdt" => diagnostics::tool_dump_crdt(&args),
|
"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)
|
// Read-only peer mesh diagnostics (story 720)
|
||||||
"mesh_status" => diagnostics::tool_mesh_status(&args),
|
"mesh_status" => diagnostics::tool_mesh_status(&args),
|
||||||
// Arbitrary pipeline movement
|
// Arbitrary pipeline movement
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ mod tests {
|
|||||||
assert!(names.contains(&"status"));
|
assert!(names.contains(&"status"));
|
||||||
assert!(names.contains(&"loc_file"));
|
assert!(names.contains(&"loc_file"));
|
||||||
assert!(names.contains(&"dump_crdt"));
|
assert!(names.contains(&"dump_crdt"));
|
||||||
assert!(names.contains(&"recover_half_written_items"));
|
|
||||||
assert!(names.contains(&"get_version"));
|
assert!(names.contains(&"get_version"));
|
||||||
assert!(names.contains(&"remove_criterion"));
|
assert!(names.contains(&"remove_criterion"));
|
||||||
assert!(names.contains(&"mesh_status"));
|
assert!(names.contains(&"mesh_status"));
|
||||||
@@ -107,7 +106,7 @@ mod tests {
|
|||||||
assert!(names.contains(&"show_epic"));
|
assert!(names.contains(&"show_epic"));
|
||||||
assert!(names.contains(&"freeze_story"));
|
assert!(names.contains(&"freeze_story"));
|
||||||
assert!(names.contains(&"unfreeze_story"));
|
assert!(names.contains(&"unfreeze_story"));
|
||||||
assert_eq!(tools.len(), 75);
|
assert_eq!(tools.len(), 74);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -288,25 +288,6 @@ pub(super) fn system_tools() -> Vec<Value> {
|
|||||||
"required": []
|
"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!({
|
json!({
|
||||||
"name": "wizard_status",
|
"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.",
|
"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.",
|
||||||
|
|||||||
Reference in New Issue
Block a user