NEC-323: Typing in a canvas note card was lost in both vaults: a stale view saved old text when asked to save before a write #314

Merged
nectenda-agent merged 12 commits from worktree-nec-323-typing-in-a-canvas-note-card-was-lost-in into main 2026-10-22 11:43:26 +01:00
Collaborator

Task: NEC-323 — https://projectron.nerchure.com/tasks/343

Typing in a canvas note card could be lost in both vaults: before a write, the guard asked every card of the note to save, and Obsidian's card save hands its text to the other whole-note cards before checking for a change, so a section card not yet re-indexed pushed the older note into the card just typed in, whose save wrote it over the typing. Only a card holding typing is asked now; a clean card is read by lastSavedData (Obsidian leaves data behind on a reload, which repeated a line on every write once the forced saves stopped); and no file is loaded over a card's typing (a repair race that lost a word). Ride-alongs, each on the user's yes: NEC-325 investigated and guarded by a test (not reproduced); NEC-326 a canvas card's save is read in against what that card built it on, when that is text read in; NEC-328 a failed download of a listed attachment version is retried, never as a download over a structured file; NEC-329 no bind backup when the document holds exactly what the editor did. Each rule has a test that fails without it; gate green at 6762082f; canvas and Markdown hardness hours and the seed 1584194348 replay pass on the final code. Spec: docs/changes/NEC-323-stale-note-card-save/spec.md.

Spec: docs/changes/NEC-323-stale-note-card-save/spec.md

Also fixes NEC-325: A canvas card of one section of a note, holding typing, wrote over another card's save of the note with its stale surroundings
Also fixes NEC-326: A save from a view that missed a write was read in against the first open view's text, not its own, and could repeat a line
Also fixes NEC-328: A new version of an image renamed 10 ms after its upload stayed old in the other vault
Also fixes NEC-329: A note's editor was backed up as it bound, with nothing of its own to lose, when a peer's paste replaced it during the handover

Changelog

Typing in a canvas card that shows a note is no longer lost when another card of the same note had not caught up yet.

Task: NEC-323 — https://projectron.nerchure.com/tasks/343 Typing in a canvas note card could be lost in both vaults: before a write, the guard asked every card of the note to save, and Obsidian's card save hands its text to the other whole-note cards before checking for a change, so a section card not yet re-indexed pushed the older note into the card just typed in, whose save wrote it over the typing. Only a card holding typing is asked now; a clean card is read by lastSavedData (Obsidian leaves data behind on a reload, which repeated a line on every write once the forced saves stopped); and no file is loaded over a card's typing (a repair race that lost a word). Ride-alongs, each on the user's yes: NEC-325 investigated and guarded by a test (not reproduced); NEC-326 a canvas card's save is read in against what that card built it on, when that is text read in; NEC-328 a failed download of a listed attachment version is retried, never as a download over a structured file; NEC-329 no bind backup when the document holds exactly what the editor did. Each rule has a test that fails without it; gate green at 6762082f; canvas and Markdown hardness hours and the seed 1584194348 replay pass on the final code. Spec: docs/changes/NEC-323-stale-note-card-save/spec.md. Spec: `docs/changes/NEC-323-stale-note-card-save/spec.md` Also fixes NEC-325: A canvas card of one section of a note, holding typing, wrote over another card's save of the note with its stale surroundings Also fixes NEC-326: A save from a view that missed a write was read in against the first open view's text, not its own, and could repeat a line Also fixes NEC-328: A new version of an image renamed 10 ms after its upload stayed old in the other vault Also fixes NEC-329: A note's editor was backed up as it bound, with nothing of its own to lose, when a peer's paste replaced it during the handover ## Changelog Typing in a canvas card that shows a note is no longer lost when another card of the same note had not caught up yet.
Before writing a note, the guard asked every canvas card of it to save.
Obsidian's card save hands the file it rebuilt to every other whole-note
card before checking whether anything changed, and a card showing one
section reloads only on the re-index. So a section card not reloaded yet,
asked to save just after another card saved typing, handed it the older
note, and that card's own save wrote it over the typing - read in as an
edit and lost in both vaults (canvas hardness seed 1584194348).

Only a card holding typing (dirty) is asked now. The e2e reproduces the
loss in Obsidian 1.13.7 with the re-index held back, and fails without
the fix; the contract case checks dirty against the running Obsidian.

Task: NEC-323
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PzFyaLhdn3QDH35KvQpHcB
Asking only a card holding typing to save (the commit before) repeated a
line on every write: Obsidian moves a card's data on a save, not on a
reload, and the asked-for saves had kept it current. A clean card read by
data looked behind every write, so the guard repaired after each and read
a section card's saves in against a stale base - eleven copies of a line
in both vaults. A clean card is now read by lastSavedData.

That moved repairs onto typing: a repair saves a view and then loads the
file into it, and a word typed between the two was overwritten, in both
vaults. A file is no longer loaded into a card holding typing, as
Obsidian's own reload never does; its next save is read in instead.

Each rule has a unit test and an e2e that fail without it. The e2e's
"not reloaded yet" check now reads lastSavedData, as review found data
could not fail it.

Task: NEC-323
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PzFyaLhdn3QDH35KvQpHcB
NEC-325, found in review and read from Obsidian's code: a section card
holding typing does not reload, so another view of the note saving the
file itself inside its 2 s would be written over by the card's next save.
Not reproduced. The likeliest such view, the note's pane bound live, is
not bound while a card is edited, so a peer's edit arrives as sync's
write, which asks the card to save first. The e2e keeps it so, and the
limitations entry says what was read and what was not reproduced.

Task: NEC-323
Also-fixes: NEC-325
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PzFyaLhdn3QDH35KvQpHcB
From the second review of NEC-323's rules. A card left holding typing
over a repair has its next save read in against what it held before the
write only while the repair's base lasts, or once a save of its own is
read in; the comment and the limitations entry now say so, and record
the case left open: typing undone back to the card's last save, inside a
repair. The data setter's half of the never-load-over-typing rule had no
test that could fail; the repair-race test now checks it. The bullet on a
card handing its file to every whole-note card, deleted earlier, is back
and accurate.

Task: NEC-323
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PzFyaLhdn3QDH35KvQpHcB
The guard read a save from a view that missed a write in against what
the first open view held. With two canvas cards of a note, a section card
first and not yet reloaded held the note from before the other card's
typing was saved; that card, typed into again, saved over the write, and
its typing came in twice.

The guard now keeps what each view held, moved on as that view's saves
are read in, and ContentSync names the text it reads in, so the base is
that of the view holding exactly that text. No match, or matches that
disagree, keep the shared base. Structured files name no text and are
read in as before. SAFE-A19 says so.

Task: NEC-323
Also-fixes: NEC-326
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PzFyaLhdn3QDH35KvQpHcB
Review of the previous commit found its per-view snapshot wrong the other
way: a view that loaded another view's save after it was read in, then
saved on top of it, was read in against its old text, and the other
view's line came in again. A test through ContentSync shows it.

Obsidian keeps what a view's next save is built on in lastSavedData, and
sets it to what it writes before writing. The note guard now wraps each
guarded view's save to read it either side (a canvas card's embedded
note through saveTarget), puts the save back on stop, and reads a save
in against what the view that wrote exactly that text built it on. No
match, or matches from different bases, keep the shared base; structured
files are read as before. Two ContentSync tests with two text views fail
with the shared base, with the snapshot, and with ContentSync not naming
the text it reads in.

Task: NEC-323
Also-fixes: NEC-326
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PzFyaLhdn3QDH35KvQpHcB
Review of the save tracking found its premise false for Obsidian's text
views: their autosave binds save before the wrap, so most saves go
unread, and a repair's setViewData leaves lastSavedData behind, so a
later save would replay the change it brought. And a chain of saves could
be read in against one never read in, dropping what that one brought.

So only canvas note cards, which name a saveTarget and whose adapter's
load moves lastSavedData, are read this way, and a card's base is used
only when it is text the document has read in or written (SAFE-A19).
Every other view is read in against what the views held as a whole, as
on main. Tests fail without the read-in rule and without tracking.

Task: NEC-323
Also-fixes: NEC-326
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PzFyaLhdn3QDH35KvQpHcB
Found by NEC-323's canvas hardness hour (seed 729611777, round 73): a new
version of an image, renamed 10 ms after its upload was listed, answered
404 to the other vault, which never asked again and kept the old bytes
under the new name, the vaults disagreeing with nothing to show it.

A version whose fetch fails - an error status or no answer - is now
fetched again after 2 s, 10 s and 60 s, at whatever path the listing has
it then (a rename moves the entry), through FileSync so it takes its turn
with the path's other work, and again on the next reconnect. A version
the listing no longer names is dropped; one that failed to open or
verify is not retried. Why the server answered 404 is not yet known.

Task: NEC-323
Also-fixes: NEC-328
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PzFyaLhdn3QDH35KvQpHcB
From review of the refetch: a fetch made to keep an older client's
version beside a structured file shares the download's fetch, and its
retry came back as a plain download, writing that version over the live
canvas or drawing (SAFE-A17). Only a plain download is retried now, and
FileSync's redownload skips paths listed as text or structured, as its
own downloads do. A version above the device's ask-first size is left
to the next connect, so a retry does not ask again. SAFE-A34 states the
retry's limits; the two negative tests now give the retry real time, so
they can fail.

Task: NEC-323
Also-fixes: NEC-328
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PzFyaLhdn3QDH35KvQpHcB
Found by NEC-323's Markdown hardness hour (seed 714381893, round 171): a
peer's paste reloaded a pane as it bound. Its logged typing replayed in
full and the document then held exactly the text the pane had, but a
reload under logged edits always kept a backup, so the bind copied what
every vault already had, flagged as a backup with no reason.

The bind now skips it only when every edit replayed and the document
equals the text kept; any other reload under logged edits keeps the
editor's text as before (SAFE-B5). Nothing was ever lost here.

Task: NEC-323
Also-fixes: NEC-329
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PzFyaLhdn3QDH35KvQpHcB
Record NEC-323's gate and hardness runs in its spec
Some checks failed
CI / promote (pull_request) Has been cancelled
CI / e2e (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
Release note / release-note (pull_request) Successful in 16s
c656186a0f
Task: NEC-323
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PzFyaLhdn3QDH35KvQpHcB
Release notes for NEC-323 and its ride-alongs
All checks were successful
Release note / release-note (pull_request) Successful in 16s
CI / build (pull_request) Successful in 6m59s
CI / e2e (pull_request) Successful in 12m49s
CI / promote (pull_request) Has been skipped
CI / build (push) Successful in 5m34s
CI / e2e (push) Successful in 12m46s
CI / promote (push) Successful in 33s
e6fe9f9f04
Task: NEC-323
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PzFyaLhdn3QDH35KvQpHcB
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Nectenda/nectenda!314
No description provided.