Closed
kayg
wants to merge 7 commits from
feat/tiering-761-a-migration into dev
pull from: feat/tiering-761-a-migration
merge into: kayg:dev
kayg:main
kayg:feat/style-sheet-fixes
kayg:storage/wal-checkpoint-off-save-1051
kayg:t3/paged-search-reconcile-final
kayg:feat/pass8
kayg:review/calendar-2
kayg:fix/shared-row-swipes
kayg:feat/contrast
kayg:dev
kayg:feat/collections-db
kayg:fix/instant-keys-1056
kayg:feat/money-round
kayg:perf/stalls-1054
kayg:wip/tiering-v3-cold4
kayg:wip/tiering-v3-cold5
kayg:port/rows-actions
kayg:port/rows-drop
kayg:port/hidden-blocks
kayg:port/rows
kayg:port/port-tables
kayg:port/port-tables-2
kayg:t3/mail-1011-liveness
kayg:port/devices
kayg:port/port-attachments
kayg:port/port-attachments-2
kayg:port/port-attachments-3
kayg:docs/design-consistency
kayg:docs/engineering-standards
kayg:fix/rusqlite-0.41-rustsec-2026-0459
kayg:feat/iphone-fixes
kayg:shared/restore-and-stale-918-919
kayg:fix/phone-top-band-contrast
kayg:feat/notes-approve
kayg:perf/soak-max-gate
kayg:feat/files-s1-identity
kayg:fix/lifecycle-event-id
kayg:feat/quickfix
kayg:wip/tiering-v3-cold
kayg:perf/web-mockup-frame-fixes
kayg:port/contacts-merge
kayg:port/contacts-edit
kayg:test/shared-room-no-deleted-text
kayg:perf/first-paint-credits
kayg:fix/shared-history-after-grant
kayg:fix/phone-swipe-regressions
kayg:feat/contrast-r1
kayg:review/sharing-pr3-review
kayg:feat/tiering-v3-s2-tags
kayg:feat/folder-drop
kayg:fix/deskpills-lazy-share
kayg:feat/sharing-s1-pr3
kayg:tmp/money-round-before
kayg:fix/phone-pane-backdrop-roots
kayg:fix/palette-preload-chips
kayg:fix/invite-defaults
kayg:port/tasks-matrix
kayg:fix/restore-green-gates
kayg:fix/sharing-fast-path-hardening
kayg:t3/photos-s1
kayg:feat/tiering-v3-s1-namespace
kayg:notes-archive-hide
kayg:feat/tasks-actions
kayg:t3/photos-plan-slice-one
kayg:feat/whiteboard-s1
kayg:t3/paged-search-reconcile
kayg:t3/smooth-pane-transitions
kayg:t3/index-position-startup-scan
kayg:fix/private-staging-759b-r2
kayg:feat/dav-transport
kayg:feat/imap-notes-fetch
kayg:feat/imap-notes-projection
kayg:feat/imap-notes-readonly
kayg:feat/imap-notes-auth
kayg:feat/imap-notes-server-foundation
kayg:docs/undo-block-ids-plan
kayg:fix/undo-768
kayg:proto/block-identity-v7d
kayg:issue/carddav-read-perf-fix-r2
kayg:issue/carddav-read-r2
kayg:docs/imap-766-plan-v2
kayg:docs/imap-notes-plan
kayg:docs/quota-plan-2
kayg:fix/parallel-test-flakes
kayg:feat/contacts-companion-pure
kayg:docs/calendar-backend-v2-plan
kayg:feat/calendar-ical-converter
kayg:docs/tasks-writes-v2-plan
kayg:docs/carddav-writes-v2-plan
kayg:docs/contacts-import-v2-plan
kayg:issue/610-pm-midnight
kayg:feat/calendar-v2-s1-containment
kayg:docs/dav-recorder-v2-plan
kayg:plan/previews-865
kayg:plan/save-log-durable-entry
kayg:fix/e2e-data-on-ram
kayg:feat/appwindow3
kayg:feat/appwindow2
kayg:feat/appwindow
kayg:feat/money-p1
kayg:feat/keys-sheet
kayg:review/calendar-2-themes
kayg:feat/smooth-cursor
kayg:fix/float-style
kayg:review/calendar-2-float
kayg:release/2026-11-06-hotfix
kayg:archive/port/rows-drop-code
kayg:archive/port/rows-actions
kayg:archive/port/rows
kayg:scratch/tasks-894-ui-gates
kayg:review/calendar-2-night-wip
kayg:release/2026-11-05
kayg:archive/feat/tiering-v3-s1-namespace
kayg:archive/flaky/rows-drop-fixed
kayg:archive/flaky/rows-actions
kayg:archive/feat/tiering-v3-reader
kayg:archive/feat/tiering-v3-roots
kayg:archive/flaky/rows
kayg:sim/bm15-final
kayg:sim/bm15-fix
kayg:sim/bm15-bis-932
kayg:sim/bm15-bis-909
kayg:sim/bm15
kayg:sim/bm15-bis-887
kayg:perf/tiering-option-b
kayg:port/style-3
kayg:port/style-2
kayg:port/style
kayg:fix/sw-lazy-precache
kayg:fix/vim-w-waits-for-save
kayg:fix/signout-test-races
kayg:fp-scratch/all
kayg:sim/bm15-first-paint
kayg:archive/bm15/rows-drop
kayg:archive/bm15/rows-actions
kayg:fix/palette-snippet-contrast
kayg:archive/bm15/rows
kayg:archive/bm15/hidden
kayg:archive/plan/save-log-durable-entry
kayg:mock/money-fix2
kayg:archive/fix/e2e-data-on-ram
kayg:mock/appearance-all
kayg:mock/pill-press
kayg:archive/docs/design-consistency
kayg:mock/auto-swipes
kayg:mock/mail-actions
kayg:archive/feat/mail-p1
kayg:mock/money-static
kayg:archive/review/standards-917
kayg:archive/docs/engineering-standards
kayg:perf/tiering-761-fix
kayg:mock/type-audit
kayg:mock/third-pane
kayg:feat/contacts-import-v2-s1
kayg:perf/executor-hot-path
kayg:archive/port/hidden-blocks
kayg:archive/port/devices
kayg:scratch/executor-on-894
kayg:scratch/executor-spans2
kayg:scratch/executor-spans
kayg:train/2026-11-03-bm14c
kayg:archive/port/rows-drop
kayg:feat/tiering-761-d-scale
kayg:feat/tiering-761-c-index
kayg:feat/tiering-761-b-reader
kayg:archive/review/sharing-pr3-review-9fec3d1f
kayg:train/2026-11-03-bm14b
kayg:perf/ports-lazy-budgets
kayg:train/2026-11-03-bm14
kayg:train/2026-11-03-bm12
kayg:archive/review/sharing-pr3-review
kayg:archive/feat/sharing-s1-pr3
kayg:train/2026-11-03-bm11
kayg:archive/feat/imap-notes-fetch
kayg:train/2026-11-03-bm10
kayg:archive/review/sharing-pr3-review-code
kayg:archive/plan/previews-865
kayg:train/2026-11-02-bm9
kayg:archive/port/port-attachments-3
kayg:archive/port/contacts-merge
kayg:fix/motion-audit-859
kayg:train/2026-11-02-bm8
kayg:archive/scratch/bm8-prep
kayg:archive/port/port-pickers
kayg:archive/port/port-tables-2
kayg:archive/port/port-tables
kayg:archive/feat/sharing-s1-pr3-code
kayg:archive/scratch/bm7-fixed
kayg:train/2026-11-02-bm7
kayg:archive/fix/parallel-test-flakes-code-minipc01
kayg:train/2026-11-02-bm6
kayg:docs/imap-766-readonly-slice
kayg:feat/dav-recorder-v3
kayg:research/788-diagnosis
kayg:archive/fix/parallel-test-flakes
kayg:archive/perf/pr792-cold-index-edit
kayg:docs/sharing-slice-1
kayg:archive/docs/imap-766-plan-v2
kayg:feat/dav-recorder-v2
kayg:archive/docs/dav-recorder-v2-plan
kayg:spike/imap-766-s0-cases
kayg:archive/fix/undo-768
kayg:feat/dav-recorder
kayg:archive/docs/contacts-import-v2-plan
kayg:archive/feat/calendar-ical-converter
kayg:feat/contacts-import
kayg:perf/cold-growth-on-764
kayg:perf/cold-growth-on-tiering
kayg:archive/docs/tasks-writes-v2-plan
kayg:archive/docs/calendar-backend-v2-plan
kayg:archive/feat/contacts-companion-pure
kayg:feat/tasks-set-status
kayg:feat/tasks-move-reorder
kayg:archive/docs/carddav-writes-v2-plan
kayg:archive/feat/calendar-v2-s1-containment
kayg:feat/tasks-delete-promote-duplicate
kayg:archive/feat/dav-transport
kayg:archive/fix/carddav-rebuild-r1
kayg:archive/fix/carddav-retention-r1
kayg:feat/carddav-writes
kayg:perf/quota-on-tiering
kayg:perf/quota-simple
kayg:perf/cold-growth-on-quota-stack
kayg:feat/calendar-identity
kayg:feat/calendar-event-parser
kayg:feat/calendar-props-maps
kayg:port/glass-when-ring
kayg:feat/tiering-761-s1
kayg:wip/10by32-01-impl-759b-scratch-20261030
kayg:wip/6by32-01-bm3-webkit-pause-20261030
kayg:wip/10by32-01-bm3-chromium-pause-20261030
kayg:wip/6by32-01-tier-761-s1-pause-20261030
kayg:spike/imap-766-s0
kayg:wip/6by32-02-calternal-pause-20261030
kayg:wip/8by64-01-review-688-pause-20261030
kayg:wip/6by32-01-verify-744-pause-20261030
kayg:wip/6by32-01-verify-742-pause-20261030
kayg:fix/private-staging-759b
kayg:wip/6by32-01-review-748-pause-20261030
kayg:wip/6by32-01-review-733c-pause-20261030
kayg:wip/6by32-01-review-733b-pause-20261030
kayg:wip/6by32-01-perf-idle-8-pause-20261030
kayg:wip/6by32-01-perf767-base-pause-20261030
kayg:wip/6by32-01-perf767b-767-pause-20261030
kayg:wip/6by32-01-perf767b-764-pause-20261030
kayg:wip/6by32-01-perf767-767-pause-20261030
kayg:wip/6by32-01-perf767-764-pause-20261030
kayg:wip/6by32-01-lists-b-pause-20261030
kayg:wip/6by32-01-ids-verify5-pause-20261030
kayg:wip/6by32-01-ids-verify4-pause-20261030
kayg:review/calendar-month
kayg:wip/10by32-02-undo-768-20261030
kayg:test/ids-verify3
kayg:wip/10by32-02-bm3-chromium-20261030
kayg:wip/10by32-01-review-753-20261030
kayg:wip/10by32-01-perf-idle-2-20261030
kayg:wip/10by32-01-bm2-webkit2-20261030
kayg:plan/759b-staging
kayg:train/2026-10-30-bm2
kayg:review/758b-tests
kayg:review/758-tests
kayg:review/764d-tests
kayg:review/767b-tests
kayg:review/preview-2026-10-29
kayg:review/767-tests
kayg:docs/tiering-731-plan
kayg:wip/c6b02-when2
kayg:wip/c6b02-tasks-create-fix5
kayg:wip/c6b02-settings-frost-mock
kayg:wip/c6b02-settings-frost-base
kayg:wip/c6b02-settings-frost
kayg:wip/c6b02-ring
kayg:wip/c6b02-review-733d
kayg:wip/c6b02-bm1-full-a
kayg:wip/compute02-when-field
kayg:wip/compute02-webkit-lists-repro
kayg:wip/compute02-train-bl2-dev
kayg:wip/compute02-dev-green-pw164
kayg:docs/deploy-plan
kayg:review/764c-tests
kayg:review/764b-tests
kayg:review/748g-tests
kayg:review/764-tests
kayg:review/759b-tests
kayg:fix/storage-owner-liveness-642
kayg:fix/storage-fix-7
kayg:wip/compute02-storage-fix-7
kayg:review/759-tests
kayg:feat/tiering-731
kayg:wip/tiering-731-first-attempt
kayg:wip/compute02-storage-fix-1
kayg:review/754g-tests
kayg:perf/idle-honest-2
kayg:review/754f-tests
kayg:review/754e-tests
kayg:review/755-tests
kayg:review/754d-tests
kayg:review/754c-tests
kayg:review/754b-tests
kayg:review/754-tests
kayg:fix/storage-private-tmp-642-dev
kayg:wip/compute02-verify-750
kayg:test/ids-verify5
kayg:test/ids-verify4
kayg:fix/storage-exact-file-quarantine-642
kayg:wip/compute02-verify-740
kayg:perf/one-flush-save
kayg:review/calendar-static
kayg:wip/compute02-verify-737
kayg:review/indexing-ring
kayg:review/when-field-2
kayg:wip/compute02-review-733
kayg:proto/block-identity-2-review
kayg:proto/block-identity-review
kayg:wip/c6b02-tc-design
kayg:review/when-field
kayg:issue/tasks-create-ids
kayg:issue/tasks-create
kayg:review/combined
kayg:wip/compute02-review-combined
kayg:review/task-polish
kayg:review/top-buttons
kayg:review/q1-q5
kayg:train/2026-10-26-bl2
kayg:archive/wt-398h
kayg:archive/train-bm-proptest-seed
kayg:archive/integ325-293
kayg:archive/integ325-ci
kayg:archive/client-auth-review-baseline
kayg:archive/wt-sim-4853
kayg:archive/wt-sim-4811
kayg:archive/wt-448m
kayg:archive/wt-398a-allocator
kayg:issue/calendar-adr
kayg:archive/old/train/2026-10-23bk2
kayg:archive/old/train/2026-10-23bk
kayg:issue/469-mockup-9a
kayg:issue/smart-tasks-mockup
kayg:issue/469-menus
kayg:issue/652-signout-flake
kayg:issue/652-history-fix7
kayg:issue/652-history-fix6
kayg:archive/old/issue/spring-check-perf
kayg:issue/spring-check-perf
kayg:issue/659-rename-fast
kayg:archive/old/train/2026-10-21ar
kayg:issue/search-hardening-3
kayg:issue/652-history-fix5
kayg:issue/660-port-sheets-glass
kayg:issue/652-history-fix4
kayg:issue/652-history-fix3
kayg:issue/652-history-fix2
kayg:issue/652-history-flake
kayg:train/2026-10-21an
kayg:issue/settings-admin-port
kayg:issue/search-port
kayg:train/2026-10-20am
kayg:issue/storage-split-fix2
kayg:issue/storage-split
kayg:archive/old/issue/folder-ids-fix
kayg:train/2026-10-20al
kayg:archive/old/issue/folder-ids
kayg:train/2026-10-20ak
kayg:issue/tables-mockup
kayg:train/2026-10-19aj
kayg:train/2026-10-19ai
kayg:issue/469-merge-fixes
kayg:issue/sidebar-band
kayg:archive/old/issue/608-settings-spec
kayg:train/2026-10-19ah
kayg:issue/swipe-offscreen
kayg:train/2026-10-19ag
kayg:archive/old/issue/590-nlp-adr
kayg:archive/old/issue/587-event-after-write-2
kayg:train/2026-10-19af
kayg:archive/old/issue/587-event-after-write
kayg:issue/469-pill-room-c
kayg:issue/469-pill-room
kayg:train/2026-10-19ae
kayg:issue/469-narrow-handle
kayg:issue/397-ios-swipe-2
kayg:archive/old/issue/adrs-photos-files-tasks
kayg:issue/469-desk-fixes
kayg:archive/old/issue/auto-update-adr
kayg:issue/469-filter-gap
kayg:issue/469-port-1b
kayg:issue/442-opacity-flake
kayg:archive/397-ios-swipe-2
kayg:issue/327-web-fast-3
kayg:archive/old/issue/469-port-1
kayg:issue/469-port-1
kayg:issue/327-web-fast-2
kayg:archive/old/issue/327-web-fast
kayg:archive/old/issue/425-stt-bench-vq1
kayg:archive/old/issue/445-no-bleed
kayg:archive/old/issue/400-swipe-mockup-2
kayg:issue/426-client-review
kayg:issue/426-client-2
kayg:issue/426-client
kayg:archive/old/issue/476-safe-area-lab
kayg:archive/445-top-light-2
kayg:archive/old/issue/445-top-light
kayg:issue/471-blocks-mockup
kayg:archive/old/issue/448-links-app-2
kayg:archive/old/issue/448-links-app
kayg:archive/old/issue/398-cold-links
kayg:issue/434-442-small-2
kayg:issue/434-442-small
kayg:archive/old/issue/398-unload
kayg:archive/old/issue/445-bottom-band
kayg:issue/445-bottom-band
kayg:issue/351-rem-3
kayg:archive/312-views-mockup
kayg:issue/351-rem-2
kayg:issue/351-rem
kayg:archive/old/issue/327-first-note
kayg:issue/395-width-phone
kayg:archive/old/issue/327-typing
kayg:issue/396-ios-edges
kayg:issue/397-ios-swipe
kayg:archive/398-allocator
kayg:archive/old/issue/327-notes-index
kayg:issue/345-347-contrast-empty-2
kayg:archive/old/issue/44-arenas
kayg:issue/44-arenas
kayg:archive/integ325
kayg:archive/old/issue/327-perf-harness
kayg:archive/old/issue/44-thp
kayg:issue/44-thp
kayg:archive/old/issue/285-sticky-pills
kayg:archive/old/issue/44-trim-3
kayg:issue/44-trim-3
kayg:archive/old/issue/165-cards
kayg:archive/old/issue/165-picker-2
kayg:archive/old/issue/212-multi-select
No reviewers
Labels
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
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
kayg/calternal!790
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/tiering-761-a-migration"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Plan — offline layout migration (#761), A of A → B → C → D
Separate production's offline migration from the reader, paged index and qualification tools in #784. A targets
dev; later PRs stack on it. No merge or production operation is authorized here.--data/--statespellings; add--userdata/--appdata. Omitted appdata resolves to the selected data root, preserving old CLI operations rather than writing a new cwd-relative./appdata. First-time split setup supplies both roots explicitly.calternal_core::layoutclassification with every state reader. Include contacts/raw, journal and SQLite side files. A real-server fixture exercises the list.--unknown-to-appdata. Preserve symlinks as symlinks; list and refuse devices/FIFOs/sockets except a stopped admin socket..split-partialtrees; discard only matching owned staging trees on retry. Verify all copied bytes and metadata; rewalk and hash the source before publication. Never merge into a published destination. Refuse completed splits; a crash after publishing only re-marks an unchanged source, without copying.Cost: O(entries + bytes) for inventory/copy/verification; O(entries + retained xattr bytes) inventory/hash bookkeeping and a 1 MiB streaming buffer. Reserve total apparent bytes + 1% on each destination filesystem (conservative when roots use separate filesystems); source remains as rollback copy. Sparse zero blocks stay holes; hard links become independent logical files. No production-data read or cutover was performed.
Failure → effect → detection → recovery → test
a_finished_split_never_runs_again_so_nothing_deleted_comes_backa_stop_at_any_step_publishes_nothing_partial_and_the_rerun_starts_overisolation::layout::every_entry_the_server_writes_is_sorted_into_the_root_it_reads_it_from, realistic fixturea_realistic_root_splits_exactly_with_times_modes_links_and_attributes,verification_keeps_access_times_on_files_directories_and_symlinks, round-trip manifesta_change_to_the_source_during_the_copy_publishes_nothingwhile_a_split_runs_no_server_can_start_on_the_old_root, root-lock replacement regressiondry_run_lists_everything_and_writes_nothing,lost_found_contents_are_left_to_the_filesystemdry_run_on_a_root_without_a_lock_does_not_create_oneunsplit_stopped_at_any_step_starts_over_and_never_publishes_half,unsplit_after_new_writes_keeps_the_current_state_and_refuses_ambiguityValidation
PASS at
5441212fd8b445fb80ff8ed640a4b33c17ae1d07on dev-02: fmt, clippy with-D warnings, and the full workspace suite; 1178 passed, 0 failed, 20 ignored, job exit 0. All heavy commands ran in Nix through~/bin/slot. Evidence:~/tier-r2-final-a.logand~/tier-r2-final-a.exit.A's final end-to-end gate on dev-02 also passed (
ALL PASS): 211 entries, 161 regular files, three users and three contact originals; 36 notes before the new write and 37 after. It verifies split, immediate unsplit, unchanged old-root refusal, new split-layout writes, unsplit against the current roots and old-binary startup.Old binary:
d83e54db. Evidence: dev-02~/tier-r2-final-a.log,~/tier-r2-final-a.exit= 0; fixture~/tier-r2-final-roundtrip.Round-2 findings
r2_published_unsplit_refuses_changed_inputs: add/edit/delete/mode changes atpublishedrefuse and preserve output. Original A failed this regression.r2_every_prepublication_fsync_close_and_rename_failure_abortsinjects at every prepublication syscall hook for split and unsplit, requires no published roots/DONE and successful retry;r2_retraction_fsync_errors_are_reported_toochecks repeated rollback errors. Independent LD_PRELOAD fsync #7 EIO: original A returned success, fixed A returns failure with no userdata/appdata/DONE.r2_cleanup_keeps_ownership_until_payload_is_goneinterrupts payload cleanup, checks OWNER survives, retries successfully. Original A failed.r2_tiny_xattrs_retain_only_their_bytesandr2_metadata_heavy_inventory_retains_actual_xattr_bytescover capacity and 4,096 one-byte attributes; original A failed. Independent 4,096-file RSS gate: original 300,524 KiB, fixed 38,820 KiB, threshold 131,072 KiB.The real-server layout test now compares every copied file, including SQLite WAL/SHM, against the immutable offline input snapshot. Live server descriptors can still mutate their own SHM after copying; comparing against those live bytes was a race.
Decisions and remaining risks
Final-head macOS checks were not run: the shared lock is held by
dav-recorder PR 789. Earlier targeted Mac results apply only to older heads. This change rejects symlinked ancestors, including aliases to the same physical root; configure physical paths. Real media errors, power cuts, 100k-note polling costs and latency qualification remain unrun.Stack: A #790 → B #791 → C #792 → D #793. Ready for final round-3 review after these fixes; nothing merged.
Independent review, round 2
Reviewed A at
c5eaf6af7a258b51ba1608b72753da3f0cd8b571, against round 1 (#784, comment 70713) and the fix table (70847). Verdict: fix first. No new P0 found. No code pushed or production data accessed.“Verified” below means the implementation and the named test actually cover the original scenario. I inspected existing gate/round-trip evidence and ran small independent reproductions on dev-01; I did not rerun the workspace suite. No builds/tests ran on the control host.
a_finished_split_never_runs_again_so_nothing_deleted_comes_backdeletes a destination note, removes DONE, retries, and verifies it stays deleted. Every-boundary test changes source before retry. Unsplit has a new analogous gap, R2-A1 below.verification_keeps_access_times_on_files_directories_and_symlinkschecks destination atime. E2E itself does not compare atime or root-directory metadata. Flush errors remain R2-A2.New findings
R2-A1, P1: published unsplit retry silently accepts stale inputs.
crates/calternal/src/storage_split/mod.rs:209-224and:264-268. A crash after output rename/parent fsync but before OWNER removal leavesMine::Yes. Restart the split-layout server, make a new note, stop it, then repeat unsplit: it removes OWNER and returns success without inventory/hash verification or copying. The advertised rollback root lacks the new note. This is the same ambiguity the split recovery digest explicitly guards against.Independent dev-01 reproduction with A's existing binary synthesized exactly that post-publication marker state, added
userdata/users/u/Notes/new.md, and retried: exit 0, “Already put back together; nothing copied”, input new file present, output new file absent. This is a boundary-state reproduction, not a SIGKILL test. Fix: bind the published rollback to both input snapshots and verify them before reporting successful recovery, otherwise refuse clearly and require a fresh destination. Never merge into that output, and account for an output that may already have been served. Add the changed-input case at thepublishedhook; the current unsplit loop retries unchanged inputs only.R2-A2, P1: observed flush failures can be ignored before successful publication.
crates/calternal/src/storage_split/tree.rs:206-211,copy.rs:98-100and:185-196. Copy closes its writable file without a checked fsync; verification explicitly ignoresfile.sync_all()errors. Later Linuxsyncfsopens a new root descriptor, so it is not a reliable substitute for reporting an already-observed writeback error (the superblock error cursor is sampled on open). Cache eviction is also best effort. A known EIO/ENOSPC must not be converted into “checked” success because subsequent reads happen to match cached bytes. Off Linux, the finalsync()provides no error result at all.Fix: propagate file durability errors, preferably sync each writable descriptor before closing it, and check the final metadata/directory flushes before publication; retain an error-observing filesystem descriptor from before writes if using batching. Add injected fsync/syncfs failures, including after final metadata, and assert no success/DONE. Current hooks inject returned errors around copy steps, not actual flush failures. This finding is source-verified; no media-error/power-cut reproduction was run.
R2-A3, P2: interrupted staging deletion loses the ownership proof.
copy.rs:172-181,mod.rs:305-325. Recursive deletion may unlink OWNER before payload directories. Kill/error during the remaining removal leaves a nonempty stage without OWNER; the next retry rejects it as foreign. Synthesizing this state on dev-01 reproduced exit 1, “was not made by this split”; original data remained intact. Fix: retain a durable cleanup ownership record until payload removal completes (or rename into a separately recorded garbage namespace), and cover interruption during cleanup. The existing hooks never interrupt deletion. The current diagnostic permits manual recovery, but “run the same command again; it starts over” is not generally true.R2-A4, P2: tiny xattrs retain 64 KiB each throughout manifests.
tree.rs:162-165.Vec::truncatechanges length, not capacity. One one-byte xattr on 100k files retains about 6.1 GiB in a single inventory, before the plan, rewalk and destination inventory coexist. A large tree can OOM even though its actual metadata is small. Source-verified, not scale-measured. Fix: allocate the returned length or shrink/box each value before retaining it; add a metadata-heavy RSS gate. The manifest is O(entries + retained attribute bytes), not just a small fixed entry record plus the 1 MiB copy buffer.Evidence and limits
ci/storage-split-e2e.pyreally diffs SHA-256 content, kind/link target, modes, uid/gid, size, nanosecond mtime and xattrs, including directory mtimes. Immediate unsplit compares against the original with no installation-marker exception; post-write unsplit compares against the current pair. It excludes operational markers, lost+found, atime and root-directory metadata; xattr read failures are flattened to an empty list. That is meaningful successful-round-trip evidence, not disk-full/power-loss proof. The Python manifest also reads each whole file into memory, so the qualification script itself is not bounded-memory for huge attachments.I checked the existing dev-01 strict E2E log, which ends ALL PASS. Existing Linux gates are author evidence, not independently rerun gates. Independent reproduction command:
netbird ssh claude@calternal-dev-01-compute02.vpn.kayg.org 'python3 -' < /tmp/tier761-round2-repro.py; fixture/tmp/tier761-r2-3ks_g5igon dev-01. Neither ENOSPC on a real filesystem, writeback EIO, SIGKILL nor power-cut ordering has been independently qualified. Exclusive renames plus parent fsync are present; a half pair at the configured final paths fails startup. Fix R2-A1/A2 before production use, and address or explicitly bound R2-A3/A4.Round-2 implementation plan (last fix before final round 3): persist a versioned, content-and-metadata input manifest in the published unsplit ownership record; unchanged recovery only, otherwise refuse and require a fresh destination. Retain checked writable file descriptors through metadata fsync and close; replace syncfs/sync batching with checked file/directory fsync, with injection at every migration flush. Keep OWNER until all staged payload is removed. Store xattr values at their actual length. O(entries + actual xattr bytes) inventories, O(bytes) copy/hash and recovery verification, 1 MiB copy/hash buffer.
Gates on exact heads: cargo fmt --all --check; cargo clippy -j4 --workspace --all-targets --locked -- -D warnings; cargo test -j4 --workspace --locked --no-fail-fast. Re-run ci/storage-split-e2e.py. Build/test through slot in Nix on dev-02. No merge.
Round-2 fixes ready for final round-3 review at
5441212fd8b445fb80ff8ed640a4b33c17ae1d07. Nothing merged.r2_published_unsplit_refuses_changed_inputs: add/edit/delete/mode changes atpublishedrefuse and preserve output. Original A failed this regression.r2_every_prepublication_fsync_close_and_rename_failure_abortsinjects at every prepublication syscall hook for split and unsplit, requires no published roots/DONE and successful retry;r2_retraction_fsync_errors_are_reported_toochecks repeated rollback errors. Independent LD_PRELOAD fsync #7 EIO: original A returned success, fixed A returns failure with no userdata/appdata/DONE.r2_cleanup_keeps_ownership_until_payload_is_goneinterrupts payload cleanup, checks OWNER survives, retries successfully. Original A failed.r2_tiny_xattrs_retain_only_their_bytesandr2_metadata_heavy_inventory_retains_actual_xattr_bytescover capacity and 4,096 one-byte attributes; original A failed. Independent 4,096-file RSS gate: original 300,524 KiB, fixed 38,820 KiB, threshold 131,072 KiB.PASS at
5441212fd8b445fb80ff8ed640a4b33c17ae1d07on dev-02: fmt, clippy with-D warnings, and the full workspace suite; 1178 passed, 0 failed, 20 ignored, job exit 0. All heavy commands ran in Nix through~/bin/slot. Evidence:~/tier-r2-final-a.logand~/tier-r2-final-a.exit. Commands inside Nix through~/bin/slot:5441212fd8b445fb80ff8ed640a4b33c17ae1d0758ffcc7182a8c7c1ff64ecc366d0e52ebe2eddf89c8c287357240912daf895c4166bf00f2212ce4a659b0f4e420f6272e801ea78eba8fe10d66c1ca5A's final end-to-end gate on dev-02 also passed (
ALL PASS): 211 entries, 161 regular files, three users and three contact originals; 36 notes before the new write and 37 after. It verifies split, immediate unsplit, unchanged old-root refusal, new split-layout writes, unsplit against the current roots and old-binary startup.Old binary:
d83e54db. Evidence: dev-02~/tier-r2-final-a.log,~/tier-r2-final-a.exit= 0; fixture~/tier-r2-final-roundtrip.Before/after evidence on dev-02:
~/tier-r2-a-red.log(four expected failures),~/tier-r2-b-red.log(two expected failures),~/tier-r2-reader-ancestor-red.log(one expected failure),~/tier-r2-fsync-probe.logand~/tier-r2-xattr-rss.log(probe wrappers exit 0). External fsync/RSS probes usede688361c, whose runtime implementation is identical to final A; the later A change fixes only the layout test's live-SHM comparison race. The final workspace gates reran after that fix and the C/D needless-borrow fix.Limits: this qualifies injected syscall failures, not real-media EIO/ENOSPC, SIGKILL or power-cut ordering. Persistent filesystem errors can prevent retraction; errors report this explicitly and retain ownership evidence for operator recovery. All outside writers must remain stopped. Final-head macOS validation was not run: shared
/tmp/mac-bench.lockremains held bydav-recorder PR 789; no other job was disturbed. No production operation or merge.C/D rows updated after C round-2 fixes. Final isolated C performance remains pending; see #792 status.
Independent review, round 3 (final)
Reviewed A at
5441212fd8b445fb80ff8ed640a4b33c17ae1d07, including round-1 comment 70713, round-2 comment 71053 and the final fix table. Verdict: ready within the documented offline migration contract.r2_published_unsplit_refuses_changed_inputsstops atpublished, then tests add/edit/delete/mode changes, requires refusal, unchanged output and retained OWNER. The unchanged-input boundary tests cover successful recovery.r2_every_prepublication_fsync_close_and_rename_failure_abortsenumerates the hooked operations for split and unsplit, requires failure with no published roots/DONE, then successful retry.r2_retraction_fsync_errors_are_reported_toocovers repeated flush errors. The recorded external fsync EIO probe also returns failure without userdata/appdata/DONE. This is not power-cut evidence.r2_cleanup_keeps_ownership_until_payload_is_goneinterrupts payload cleanup, checks OWNER survives and reruns successfully.New findings, P0–P3: none. I also checked the new marker writes, publication/retraction ordering and cleanup paths for loss, duplication or stranded data. The fixes preserve the original roots and do not merge into published output. The prior completed-split and contacts placement protections remain present.
Independent verification on dev-01, at downstream D
659b0f4econtaining these fixes, inside Nix through~/bin/slot:22 passed, zero failed, including all six A regressions. Evidence:
~/tier-r3-independent.{log,exit}, exit 0. I inspected A's exact-head gate/E2E log on dev-02, endingALL PASS, exit 0, and the recorded failing-before and external fsync/RSS probe logs. The full workspace suite and E2E were not independently rerun. No build/test ran on the control host; no code was pushed or production data accessed.Open qualification limits remain explicit: final-head macOS checks, real-media EIO/ENOSPC, SIGKILL and power-cut ordering are unrun. The operator must stop all outside writers, keep an independent backup and follow the copy-of-data dry-run/cutover runbook. Readiness here is a code-review verdict, not a claim that those campaigns passed.
A on dev
41f2efa3: resolutions and the prod-copy dry runHead
91c034ebb36ca42a19530c50996244e892e4acbe, unchanged by this takeover (the earlier agent's rebase was already on41f2efa3; its merge-base is41f2efa3).Resolutions (kept dev's side everywhere it overlaps)
layout::NOTESstate classification: dev's, kept. The split sorts with the samecalternal_core::layoutlist the server opens state with, so a name the server treats as app state can never land in userdata.git diff origin/dev <D head> -- crates/notes/src/peek.rs crates/notes/src/meta/restart.rs crates/notes/src/write_executor/recovery.rsis empty for the whole stack.Migration dry run on a copy of prod (prod never written)
Source. The deploy backup
backup-pre-41f2efa3-20261103T041034807656485Z.tgzholds app state only (docs/deploy.md): 712 note rows but no note files. So the copy has two parts:0f9f30d2…on both ends);tarof the prod Home trees (nice/ionice, app-state names excluded), streamed to dev-01 (SHA-256df415354…on both ends, 18.4 MB).Combined: 1 person, 1,291 entries (15 folders, 1,276 files), 48.35 MB. Prod was only read.
Method.
crates/calternalrelease binary at the D tree, run as the documented offline, copy-only steps (storage split --dry-run,--confirm-stopped, thenstorage unsplit). Before/after manifests hash every file and compare type, size, SHA-256, mode, mtime and link target. Servers ran on loopback with--dev-user, and comparisons go through the API:notes.list(all pages, bothshowmodes),notes.gettext hashes,notes.history,search.queryfor 40 title-derived terms,notes.rename+notes.undo.storage split --dry-runstorage split --confirm-stopped.calternal-split-donehistory.db/meta.db/auth.dbin userdatastorage unsplitround tripdone, undodone, every path restored, same 713 ids; the renamed note's body byte-identical, only itslast editedvalue changes. Same as dev on the single root.storage unsplit, then the pre-split dev binary³ The one history row that differs is today's daily note, whose version time is set at server start. It differs the same way between two dev copies started at different times.
Seen in dev as well (not tiering): on this data, the second start re-describes 273 list titles that came from the 04:10
meta.db. Dev41f2efa3does the same, and head matches dev at every start.Prod migration (not run; needs an image that contains A):
Expected stopped time: the split took 99 s for these 1,291 entries on dev-01 (one fsync per file), so plan ~2 min split + ~1 min stop/start ≈ 3 min. First start: healthy in 0.2 s, discovery done in 1.6 s.
Clean-up. Every restored tree is deleted (each script's EXIT trap). The two transfer files on dev-01 were deleted at the end. No note content was printed or posted: only counts, hashes of whole archives, timings and pass/fail.
Gate table — stack on dev
41f2efa3(takeover after the Codex usage limit)Heads: #790 A
91c034eb→ #791 Befcaaaf7→ #792 C487e0589→ #793 Dd422d0ca. Base: dev41f2efa387b74175971a838f602535089d0d5fb4(what prod runs). Nothing merged.~/bin/slot)91c034ebefcaaaf7487e0589d422d0cacargo fmt --all --checkcargo clippy -j4 --workspace --all-targets --locked -- -D warningscargo test -j4 --workspace --locked -- --test-threads=2¹ Run by the earlier agent at
89989e62;git diff 89989e62 d422d0cais empty (same tree).² Run at
896babb4;487e0589differs only by the order of two#[cfg(test)] modlines (cargo fmt).Perf VMs (binaries built on dev-01 in release at the D tree, relayed through the control host, SHA-256 checked on both perf VMs: C
2006ab8e…, dev724535bd…, harness980045d9…):41f2efa3notes.append, 30 alternating pairs, perf-02, median totalProtocol unchanged from the
fab01191run:~/bin/perf-lock ~/bin/slot run-perf.sh, per samplecalternal-perf --sizes 100k --runs 1 --cold-runs {1,1,1,0} --seed 1 --idle-secs 60 --budgets {"open.note@100000":{"p95":15},"idle.rss@100000":{"p95":100}}, C then dev, same VM, no page-cache drop; all 8 reports have no scenario failures. No limit was changed.Notes:
fab01191run (deva55521f912.6 s → dev41f2efa320.9 s on the same harness and VM), so only the ratio is comparable. The absolute memory numbers are from a different source (harnesscold.mem_peakon perf-01 vs one dev-01 sample earlier); the ratio to dev improved.~/Developer/perf-results/pr792-41f2efa3/. Perf VM binaries and work dirs deleted.Perf pass on the stack (branch
perf/tiering-761-fix, head3a65e27c): nothing in this PR's files changed. A/B tables, causes and the still-open cold-index/first-page gap are on #792 (comment above) and #793. The stack stays not mergeable under the no-regressions rule until the gap on #792 is closed or kayg decides.kayg referenced this pull request2026-11-05 23:21:11 +00:00
housekeeping #1009: needs-decision.
Old two-root split/unsplit stack conflicts with tiering v3 three-root architecture (#968/#970/#1008). Retain migration/race fixtures and offline-lock evidence.
original head:
91c034ebb36ca42a19530c50996244e892e4acbe. against devcd7f84432febfc63b788fbb9b9f9950452982d74, git cherry reports 11 unmatched and 0 equivalent patches. the landing audit lists each candidate missing commit and stable patch match inpr-790.md; unmatched patches can include superseded behavior.left open, source branch unchanged. no rebase or new test claim for this decision lane.
Superseded by tiering v3 (#968/#970/#1008). The branch and original tips are archived under
archive/(see #1009), so nothing is lost; useful regression vectors are noted there for the active lanes. Closing.Pull request closed