Skip to content

docs(storage): record why a moved device number cannot be repaired on its own - #1868

Merged
hqhq1025 merged 4 commits into
apache:mainfrom
hqhq1025:pr/storage-root-remount
Aug 3, 2026
Merged

docs(storage): record why a moved device number cannot be repaired on its own#1868
hqhq1025 merged 4 commits into
apache:mainfrom
hqhq1025:pr/storage-root-remount

Conversation

@hqhq1025

@hqhq1025 hqhq1025 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

This PR started out trying to stop the storage-root repair dialog from asking a
person about a device number they cannot answer for, and ended up concluding
that the question cannot be answered from the marker at all. What is left is the
reasoning and a tripwire, not a behaviour change.

What happened

The first attempt classified a marker mismatch as remounted when the inode
matched and only the device number had moved, and repaired it without asking.
That is wrong. A differing device number is exactly the cross-volume case, and
inode numbers are only unique within one mounted filesystem — so a workspace
restored onto another volume can present the same inode as the original. The
auto-repair would then hand back the original root id for a genuinely different
root, silently, in the one situation where being wrong costs the most.

Nothing in the marker separates those two cases, so the classification was
removed rather than narrowed. Every mismatch is a question for the person again,
which is what main already does.

What this PR now contains

  • A comment on markerMatchesIdentity recording why the device number cannot be
    trusted on its own, placed where the next person will be tempted to trust it,
    and pointing at adoptStorageRootOnImport as the sanctioned path for a copy.
  • Tests that pin the current behaviour, including the case that must keep
    asking. They guard against the auto-repair being reintroduced; they do not
    guard anything this PR adds.

What is still open

A routine volume remount still parks the launch at a repair dialog. Solving it
needs an identity signal the marker does not currently carry — a volume UUID, or
an explicit adopt step — not a cleverer reading of dev and ino. That is a
separate change and is not attempted here.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1. The E2E fixture now exercises the auto-heal branch. apps/desktop/e2e/storage-root-conflict.spec.ts:69-73 stales only dev (inode intact), which is precisely onlyTheDeviceNumberMoved's condition. The repair dialog never opens, the [storage-root] root-identity conflict gate signal never prints, appParkedAtRepairGate times out at 30s, and the marker-untouched assertion fails because the marker was rewritten. The CI plan guarantees the desktop e2e job runs for this PR. Stale ino in the spec instead, mirroring the PR's own foreign-inode fixture.

P2. ino equality does not prove a remount (raised independently by both reviewers). Inode numbers are unique only within one mounted filesystem; a workspace copied to another volume can get the same root-directory inode, and the code then silently keeps the old rootId and rewrites the marker, so two independent roots share one logical identity. The (dev, ino) pair plus a durable volume identifier would make both this case and the remount case decidable; that belongs in @maka/storage/root-authority, not the desktop layer.

P2. The classification is not bound to the repaired candidate. onlyTheDeviceNumberMoved reads and stores a boolean (storage-root-startup.ts:44,65), then a separate snapshot is prepared (:66-69), then the repair commits based on the stale boolean (:71-73). If the marker is swapped between the two reads, a foreign candidate is adopted without confirmation. Carry the classification on the repair candidate itself.

P3. The remount check re-reads and re-parses the storage-owned marker with a raw unbounded readFile (storage-root-startup.ts:37-44), bypassing readBoundedMarkerFile's 1KB bound and the schema validation; a marker replaced with a FIFO blocks forever, the exact hang this PR exists to fix. The same unanswerable condition also stays fatal for headless (packages/headless/src/headless-storage.ts:51), while storage's adoptStorageRootOnImport already auto-repairs drift. Expose the classification from the storage layer instead of re-deriving it.

@hqhq1025
hqhq1025 force-pushed the pr/storage-root-remount branch from 1a07121 to f1f7634 Compare August 2, 2026 08:14
…answer for

The storage-root marker records `dev` and `ino`. Copying a workspace gives the
copy its own inode, which is the case the identity check exists to catch and
the only one worth stopping a person for. A device number carries no such
meaning across restarts: the kernel hands one out per mount, so an unmoved
directory reports a different `dev` after its volume is mounted again while
keeping the inode it always had.

Asking about that is worse than useless. It is routine, there is nothing for a
person to decide, and the question arrives through a dialog raised before any
window exists — which does not appear. The launch hangs with the process
alive and nothing printed, which is what this looked like from the outside.

The check stays exactly as strict where it means something: `markerMatchesIdentity`
is untouched, so a genuinely copied workspace still reaches the person. Only
the desktop recovery layer learns to recognise the one case that carries no
information, and it returns false whenever the answer is not certain.

Adds one test and rewrites none: the existing assertion that an ambiguous
marker still asks is the behaviour this must not change.
It runs before any window exists, which is the only place it could be read,
and it carries a fixed sentence with no path, no identity and no secret.
… a second time downstream

Review found three problems with the desktop-side version, and all three come
from the same choice: classifying the drift by re-reading the marker after the
storage layer had already read it.

- The e2e spec stales `dev` and leaves `ino` intact, which is precisely the
  auto-repair condition. The repair dialog would never open, the gate signal
  never print, and the test would wait 30s for a hang that had been fixed. It
  stales the inode now, which is the drift that should reach a person.
- The classification was read into a boolean, a separate snapshot was then
  prepared, and the repair committed on the stale boolean. A marker swapped
  between the two reads would adopt a foreign candidate with nobody asked.
- The re-read used a raw unbounded `readFile`, bypassing the bounded read and
  schema validation the storage layer applies — a marker replaced with a FIFO
  would block forever, which is the exact hang this change exists to remove.

`StorageRootIdentityRepairCandidate` now carries `drift`, decided from the
marker it was prepared against, under the identity assertions that already
guard that read. The desktop layer reads the answer instead of deriving it.

`drift` is a reason rather than a boolean because inode equality does not
prove a remount: inode numbers are unique only within one mounted filesystem,
so a workspace copied to another volume can carry the same root-directory
inode. That case is `unknown` and still reaches the person. Naming it a
remount would silently give two independent roots one logical identity.
@hqhq1025
hqhq1025 force-pushed the pr/storage-root-remount branch from f1f7634 to 28835c7 Compare August 3, 2026 06:08
… looks the same

Repairing a moved `dev` without asking rested on the claim that only a
remount produces one. It does not. Inode numbers are unique within a single
mounted filesystem, so a workspace restored onto another volume can report the
same root-directory inode as the original — a different `dev` beside a
matching `ino`, which is exactly the pattern that was being auto-repaired.
Startup would then rewrite the marker with no dialog and hand back the
original rootId, leaving two unrelated directories sharing one identity. The
docstring said that case was a question for the person; the code answered it.

Nothing in the marker can tell the two apart, so the classification is gone
rather than narrowed, and the drift is a question again. The comment now sits
on `markerMatchesIdentity`, where the next person will be tempted, and points
at `adoptStorageRootOnImport` as the way in for a copy — where the caller
states which rootId it expects.

A desktop test pins it: a marker whose only stale field is `dev` reaches the
person, and declining leaves the marker byte for byte as it was. Restoring
the auto-repair reddens that test and two that were already there, which is
why the original change had to rewrite them.
@hqhq1025

hqhq1025 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Review finding confirmed, and the auto-repair is gone.

The claim the change rested on was that a moved device number can only come from a remount. It cannot. Inode numbers are unique within one mounted filesystem, so a workspace restored onto an external volume can report the same root-directory inode as the original. That presents as a different dev beside a matching ino, which is exactly the pattern onlyTheDeviceNumberMoved recognised, so startup would rewrite the marker with no dialog and hand back the original rootId. Two unrelated directories would then share one identity, silently. The docstring already said that case was a question for the person; the code answered it.

Nothing in the marker distinguishes the two, so I removed the classification rather than narrowing it. There is no drift field and no second answer downstream, and every identity mismatch is a question again. The explanation now sits on markerMatchesIdentity, where the next person will be tempted to add this back, and points at adoptStorageRootOnImport as the sanctioned way in for a copied workspace, where the caller states which rootId it expects.

What remains is the test that pins the claim: a marker whose only stale field is dev reaches the person, and declining leaves the marker byte for byte as it was. Negative control: putting the auto-repair back reddens that test (expected true, actual false on "only the person can tell a remount from a restored copy") and also reddens the two tests that were already there for declining and for a swapped root. Those two are the ones the original change had to rewrite from dev to ino, which in hindsight was the signal.

The complaint behind the PR is real and still open: the repair dialog is raised before any window exists, so it may not appear and the launch hangs with nothing printed. That is a dialog problem, not a reason to adopt a root without asking. #1869 at least makes that hang say which step it is stuck on. A proper fix belongs in how the question is asked, not in skipping it.

Test delta against origin/main on this machine: storage 241 failing before and after, identical failure set (all node:sqlite, missing in this local Node). Desktop 73 failing before and after; the sets differ only by one load-flaky suite that passes when run alone.

@hqhq1025 hqhq1025 changed the title fix(desktop): stop asking a person about a device number they cannot answer for docs(storage): record why a moved device number cannot be repaired on its own Aug 3, 2026
@hqhq1025
hqhq1025 merged commit 2030460 into apache:main Aug 3, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants