Atomic Save-via-Rename and Temporary File Reconciliation

Living Document Notice
Published 2026-09-15. The evolving architecture, revisions, and connected notes for this dispatch live in the Stax Digital Garden.

Atomic Save-via-Rename and Temporary File Reconciliation

Summary

Desktop text editors avoid partial in-place file corruption by writing updated buffers to hidden temporary swapfiles before executing an atomic POSIX rename onto the target path. For background storage indexers, this save pattern produces asynchronous bursts of creation, modification, and deletion events across dotfiles. Bosun PKM implements a deterministic reconciliation pipeline that ignores ephemeral staging buffers while capturing rename completions without stripping leading dots or corrupting file identity.

The Anatomy of Modern Editor Saves

When a user triggers save in Neovim, VS Code, or Obsidian, the editor executes a defensive filesystem dance designed to survive power loss:

Editor Save Protocol:
1. Open scratch file:      open(".chapter-one.md.tmp", O_WRONLY | O_CREAT | O_TRUNC)
2. Write updated buffer:   write(fd, buffer, length)
3. Flush to disk platter:  fsync(fd)
4. Close descriptor:       close(fd)
5. Atomic replacement:     rename(".chapter-one.md.tmp", "chapter-one.md")

To a file watcher, this sequence registers as three distinct filesystem events:

  • Event A: CREATE on .chapter-one.md.tmp
  • Event B: MODIFY on .chapter-one.md.tmp
  • Event C: RENAME (or MOVED_TO) from .chapter-one.md.tmp to chapter-one.md

If the background watcher eagerly indexes Event A or B, it records a temporary dotfile that disappears milliseconds later. If Event C is treated as a deletion followed by creation, relational foreign keys and wikilink edge weights in database projections thrash unnecessarily.

Storage Manager Rename Reconciliation

The Bosun storage manager handles atomic renames through explicit state reconciliation:

def record_rename(self, old_rel_path: str, new_rel_path: str) -> None:
    # 1. Detect if the source was an ignored temporary buffer
    if self.is_transient_editor_buffer(old_rel_path):
        # Treat as a clean novel write or modification on the target path
        dest_path = self.locker_root / new_rel_path
        if dest_path.exists():
            content_hash = self.compute_sha256(dest_path)
            self.upsert_note_record(new_rel_path, content_hash)
        return

    # 2. Legitimate user rename of an existing note
    with self.db_transaction() as cursor:
        cursor.execute(
            "UPDATE notes_index SET path = ? WHERE path = ?",
            (new_rel_path, old_rel_path)
        )
        cursor.execute(
            "UPDATE link_edges SET source_path = ? WHERE source_path = ?",
            (new_rel_path, old_rel_path)
        )
        # Update outbox history without breaking causal links
        self.outbox.record_rename_event(old_rel_path, new_rel_path)

By distinguishing transient editor swapfiles from genuine user-driven renames, the manager prevents transient filenames from polluting search queries and projection caches.

Preserving Dotfiles and Hidden Directories

A common bug in note-taking synchronization tools is naively stripping all leading dots from filenames, assuming any dotfile is a temporary swap buffer. This destroys legitimate user dotfiles such as .cursorrules, .gitignore, or configuration manifests located inside the .bosun metadata directory:

pub fn classify_path(relative_path: &Path) -> PathClassification {
    let file_name = match relative_path.file_name().and_then(|n| n.to_str()) {
        Some(name) => name,
        None => return PathClassification::Ignored,
    };

    // Explicit system configuration manifests
    if relative_path.starts_with(".bosun") {
        return PathClassification::SystemMetadata;
    }

    // Ephemeral editor swap patterns
    if file_name.ends_with(".tmp")
        || file_name.ends_with('~')
        || (file_name.starts_with(".#"))
        || (file_name.starts_with('.') && file_name.ends_with(".swp"))
    {
        return PathClassification::TransientBuffer;
    }

    // Standard knowledge note
    if relative_path.extension().map_or(false, |ext| ext == "md") {
        return PathClassification::KnowledgeNote;
    }

    PathClassification::Ignored
}

The classifier routes .bosun/locker.json to the system metadata validator while cleanly dropping transient swapfiles.

Performance Under High-Frequency Saves

Testing 2,000 rapid atomic save-via-rename operations across an active editor session:

Operational Metric Naive Ingestion (All Events) Atomic Reconciliation Pipeline
Total Watcher Events Emitted 6,000 events 6,000 events
Database Transactions Executed 6,000 transactions 2,000 transactions (Zero swapfile writes)
Spurious Delete/Create Cycles 2,000 churn cycles 0 churn cycles
Index Latency per Save (p99) 14.8 ms 1.1 ms

The reconciliation pipeline eliminates 66% of redundant database transactions while guaranteeing zero lost edits during rapid typing sessions.

← Back to Bosun PKM Tools Blog