Skip to content

Commit 5f0430d

Browse files
devwhodevsclaude
andcommitted
fix: release store lock before frontmatter I/O, wrap remove_file in txn
Moves handler now releases the store lock before writing frontmatter to disk, reducing MCP latency. remove_file wrapped in transaction for crash safety. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 95805b5 commit 5f0430d

2 files changed

Lines changed: 110 additions & 89 deletions

File tree

src/indexer.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,12 +399,22 @@ pub fn remove_file(rel_path: &str, store: &Store) -> Result<()> {
399399
let file = store
400400
.get_file(rel_path)?
401401
.ok_or_else(|| anyhow!("File not found: '{}'", rel_path))?;
402+
403+
let owns_transaction = store.conn().is_autocommit();
404+
if owns_transaction {
405+
store.conn().execute_batch("BEGIN DEFERRED")?;
406+
}
407+
402408
let vector_ids = store.get_vector_ids_for_file(file.id)?;
403409
for &vid in &vector_ids {
404410
store.delete_vec(vid)?;
405411
}
406412
store.delete_fts_chunks_for_file(file.id)?;
407413
store.delete_file(file.id)?;
414+
415+
if owns_transaction {
416+
store.commit()?;
417+
}
408418
Ok(())
409419
}
410420

src/watcher.rs

Lines changed: 100 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -462,116 +462,127 @@ pub async fn run_consumer(
462462
.to_string_lossy()
463463
.to_string();
464464

465-
let store_guard = store.lock().await;
466-
match indexer::rename_file(&old_rel, &new_rel, &store_guard) {
467-
Ok(()) => {
468-
tracing::info!(from = %old_rel, to = %new_rel, "renamed file in index");
469-
// Track the file_id for edge rebuild
470-
if let Ok(Some(record)) = store_guard.get_file(&new_rel) {
471-
affected_file_ids.push(record.id);
472-
}
465+
// Phase 1: Store operations under lock
466+
let needs_frontmatter_strip = {
467+
let store_guard = store.lock().await;
468+
match indexer::rename_file(&old_rel, &new_rel, &store_guard) {
469+
Ok(()) => {
470+
tracing::info!(from = %old_rel, to = %new_rel, "renamed file in index");
471+
// Track the file_id for edge rebuild
472+
if let Ok(Some(record)) = store_guard.get_file(&new_rel) {
473+
affected_file_ids.push(record.id);
474+
}
473475

474-
// Placement correction detection
475-
if let Ok(content) = std::fs::read_to_string(to) {
476-
let actual_folder = std::path::Path::new(&new_rel)
477-
.parent()
478-
.map(|p| p.to_string_lossy().to_string())
479-
.unwrap_or_default();
480-
481-
match placement::detect_correction_from_frontmatter(
482-
&content,
483-
&actual_folder,
484-
) {
485-
Some(correction) => {
486-
tracing::info!(
487-
file = %new_rel,
488-
suggested = %correction.suggested_folder,
489-
actual = %correction.actual_folder,
490-
"placement correction detected"
491-
);
476+
// Placement correction detection
477+
if let Ok(content) = std::fs::read_to_string(to) {
478+
let actual_folder = std::path::Path::new(&new_rel)
479+
.parent()
480+
.map(|p| p.to_string_lossy().to_string())
481+
.unwrap_or_default();
492482

493-
// Compute mean vector from file chunks
494-
if let Ok(Some(file)) = store_guard.get_file(&new_rel)
495-
&& let Ok(vectors) =
496-
store_guard.get_chunk_vectors_for_file(file.id)
497-
&& !vectors.is_empty()
498-
{
499-
let dim = vectors[0].len();
500-
let mut mean = vec![0.0f32; dim];
501-
for v in &vectors {
502-
for (i, val) in v.iter().enumerate() {
503-
mean[i] += val;
483+
match placement::detect_correction_from_frontmatter(
484+
&content,
485+
&actual_folder,
486+
) {
487+
Some(correction) => {
488+
tracing::info!(
489+
file = %new_rel,
490+
suggested = %correction.suggested_folder,
491+
actual = %correction.actual_folder,
492+
"placement correction detected"
493+
);
494+
495+
// Compute mean vector from file chunks
496+
if let Ok(Some(file)) = store_guard.get_file(&new_rel)
497+
&& let Ok(vectors) =
498+
store_guard.get_chunk_vectors_for_file(file.id)
499+
&& !vectors.is_empty()
500+
{
501+
let dim = vectors[0].len();
502+
let mut mean = vec![0.0f32; dim];
503+
for v in &vectors {
504+
for (i, val) in v.iter().enumerate() {
505+
mean[i] += val;
506+
}
507+
}
508+
let n = vectors.len() as f32;
509+
for val in &mut mean {
510+
*val /= n;
504511
}
505-
}
506-
let n = vectors.len() as f32;
507-
for val in &mut mean {
508-
*val /= n;
509-
}
510512

511-
// Adjust centroids: boost actual, decay suggested
512-
if let Err(e) = store_guard.adjust_folder_centroid(
513-
&correction.actual_folder,
514-
&mean,
515-
true,
516-
) {
517-
tracing::warn!(error = %e, "failed to adjust actual folder centroid");
513+
// Adjust centroids: boost actual, decay suggested
514+
if let Err(e) = store_guard.adjust_folder_centroid(
515+
&correction.actual_folder,
516+
&mean,
517+
true,
518+
) {
519+
tracing::warn!(error = %e, "failed to adjust actual folder centroid");
520+
}
521+
if let Err(e) = store_guard.adjust_folder_centroid(
522+
&correction.suggested_folder,
523+
&mean,
524+
false,
525+
) {
526+
tracing::warn!(error = %e, "failed to adjust suggested folder centroid");
527+
}
518528
}
519-
if let Err(e) = store_guard.adjust_folder_centroid(
529+
530+
// Log the correction
531+
if let Err(e) = store_guard.insert_placement_correction(
532+
&new_rel,
520533
&correction.suggested_folder,
521-
&mean,
522-
false,
534+
&correction.actual_folder,
523535
) {
524-
tracing::warn!(error = %e, "failed to adjust suggested folder centroid");
536+
tracing::warn!(error = %e, "failed to log placement correction");
525537
}
526-
}
527538

528-
// Log the correction
529-
if let Err(e) = store_guard.insert_placement_correction(
530-
&new_rel,
531-
&correction.suggested_folder,
532-
&correction.actual_folder,
533-
) {
534-
tracing::warn!(error = %e, "failed to log placement correction");
535-
}
536-
537-
// Strip placement frontmatter and write atomically
538-
let stripped =
539-
placement::strip_placement_frontmatter(&content);
540-
if stripped != content {
541-
let tmp = to.with_extension("md.tmp");
542-
if let Err(e) = std::fs::write(&tmp, &stripped)
543-
.and_then(|_| std::fs::rename(&tmp, to))
544-
{
545-
tracing::warn!(error = %e, "failed to strip placement frontmatter");
546-
let _ = std::fs::remove_file(&tmp);
547-
}
548-
}
549-
}
550-
None => {
551-
// Check if it's a confirmation (suggested == actual) — just strip
552-
let has_suggested = content.contains("suggested_folder:");
553-
if has_suggested {
539+
// Signal that frontmatter strip is needed (done outside lock)
554540
let stripped =
555541
placement::strip_placement_frontmatter(&content);
556542
if stripped != content {
557-
let tmp = to.with_extension("md.tmp");
558-
if let Err(e) = std::fs::write(&tmp, &stripped)
559-
.and_then(|_| std::fs::rename(&tmp, to))
560-
{
561-
tracing::warn!(error = %e, "failed to strip placement frontmatter on confirmation");
562-
let _ = std::fs::remove_file(&tmp);
543+
Some(stripped)
544+
} else {
545+
None
546+
}
547+
}
548+
None => {
549+
// Check if it's a confirmation (suggested == actual) — just strip
550+
let has_suggested = content.contains("suggested_folder:");
551+
if has_suggested {
552+
let stripped =
553+
placement::strip_placement_frontmatter(&content);
554+
if stripped != content {
555+
Some(stripped)
556+
} else {
557+
None
563558
}
559+
} else {
560+
None
564561
}
565562
}
566563
}
564+
} else {
565+
None
567566
}
568567
}
568+
Err(e) => {
569+
tracing::warn!(from = %old_rel, to = %new_rel, error = %e, "failed to rename file");
570+
None
571+
}
569572
}
570-
Err(e) => {
571-
tracing::warn!(from = %old_rel, to = %new_rel, error = %e, "failed to rename file");
573+
}; // store_guard dropped here
574+
575+
// Phase 2: Frontmatter file I/O without store lock.
576+
// The write triggers a Changed event that gets re-indexed anyway.
577+
if let Some(stripped) = needs_frontmatter_strip {
578+
let tmp = to.with_extension("md.tmp");
579+
if let Err(e) = std::fs::write(&tmp, &stripped)
580+
.and_then(|_| std::fs::rename(&tmp, to))
581+
{
582+
tracing::warn!(error = %e, "failed to strip placement frontmatter");
583+
let _ = std::fs::remove_file(&tmp);
572584
}
573585
}
574-
drop(store_guard);
575586
}
576587

577588
WatchEvent::FullRescan => {

0 commit comments

Comments
 (0)