Skip to content

fix(studio): rebase the debounced write on the current document - #256

Merged
LeadcodeDev merged 1 commit into
chantier/audit-2026-09from
fix/studio-stale-write
Sep 22, 2026
Merged

LeadcodeDev merged 1 commit into
chantier/audit-2026-09from
fix/studio-stale-write

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Severity Medium, category correctness. Location: crates/rustmotion-studio/src/editor/inspector.rs:2119

Impact

write_prop/write_root_field/write_content capture m.raw (the ENTIRE document) at edit time, then 250 ms later serialize that snapshot over the whole file. If the notify watcher reloads the model from an agent/editor write during that window, the payload's raw is already stale and the flush wipes the agent's change completely — not just the edited pointer. write_and_note then registers the result in the self-write ledger, so app/mod.rs:141 makes the watcher skip the resulting event: memory holds the agent's version, disk holds the studio's stale version, and nothing ever reconciles them. No error is shown (m.write_error = None on success). This is the crate's headline workflow (baseline/diff review of agent edits), so the race is routine, not exotic.

Fix

Before flushing, re-read the file and verify it still matches what the payload was derived from (hash the source at snapshot time, compare at write time); on mismatch, re-apply the single pointer mutation to the CURRENT disk content instead of replaying the stale whole-document snapshot, or surface a conflict in write_error. Do not register a self-write for a flush whose base no longer matches the disk.

Evidence the audit read

let task = spawn(async move {
    tokio::time::sleep(std::time::Duration::from_millis(250)).await;
    *debounce_slot.borrow_mut() = None;
    let snapshot = std::fs::read_to_string(payload.path()).ok();
    let result = perform_write(&payload);
// ... perform_write, JSON branch:
} else if let Some(updated) = set_field_value(raw.clone(), pointer, field, value.clone()) {
    let s = serde_json::to_string_pretty(&updated).map_err(|e| format!("json: {e}"))?;
    write_and_note(path, &s)?;

Based directly on the chantier branch.

Part of the September 2026 audit remediation chantier. Refs #220 (RM-13).

@LeadcodeDev LeadcodeDev added the bug Something isn't working label Sep 21, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 21, 2026
@LeadcodeDev
LeadcodeDev force-pushed the fix/studio-stale-write branch 2 times, most recently from 3ba4914 to 64683cc Compare September 22, 2026 08:35
write_prop/write_root_field/write_content capture m.raw (the ENTIRE
document) at edit time, then 250 ms later serialize that snapshot over the
whole file. If the notify watcher reloads the model from an agent/editor
write during that window, the payload's raw is already stale and the flush
wipes the agent's change completely — not just the edited pointer.
write_and_note then registers the result in the self-write ledger, so
app/mod.rs:141 makes the watcher skip the resulting event: memory holds the
agent's version, disk holds the studio's stale version, and nothing ever
reconciles them. No error is shown (m.write_error = None on success). This
is the crate's headline workflow (baseline/diff review of agent edits), so
the race is routine, not exotic.

Fix: Before flushing, re-read the file and verify it still matches what the
payload was derived from (hash the source at snapshot time, compare at write
time); on mismatch, re-apply the single pointer mutation to the CURRENT disk
content instead of replaying the stale whole-document snapshot, or surface a
conflict in write_error. Do not register a self-write for a flush whose base
no longer matches the disk.

Refs #220
@LeadcodeDev
LeadcodeDev force-pushed the fix/studio-stale-write branch from 64683cc to 85f344b Compare September 22, 2026 08:45
@LeadcodeDev
LeadcodeDev merged commit 8358879 into chantier/audit-2026-09 Sep 22, 2026
2 of 3 checks passed
@LeadcodeDev
LeadcodeDev deleted the fix/studio-stale-write branch September 22, 2026 08:53
LeadcodeDev added a commit that referenced this pull request Sep 22, 2026
write_prop/write_root_field/write_content capture m.raw (the ENTIRE
document) at edit time, then 250 ms later serialize that snapshot over the
whole file. If the notify watcher reloads the model from an agent/editor
write during that window, the payload's raw is already stale and the flush
wipes the agent's change completely — not just the edited pointer.
write_and_note then registers the result in the self-write ledger, so
app/mod.rs:141 makes the watcher skip the resulting event: memory holds the
agent's version, disk holds the studio's stale version, and nothing ever
reconciles them. No error is shown (m.write_error = None on success). This
is the crate's headline workflow (baseline/diff review of agent edits), so
the race is routine, not exotic.

Fix: Before flushing, re-read the file and verify it still matches what the
payload was derived from (hash the source at snapshot time, compare at write
time); on mismatch, re-apply the single pointer mutation to the CURRENT disk
content instead of replaying the stale whole-document snapshot, or surface a
conflict in write_error. Do not register a self-write for a flush whose base
no longer matches the disk.

Refs #220
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant