Tasks write actions for the UI port: move, reorder, rename, version, bulk move, delete, duplicate (on the executor) #894
Open
kayg
wants to merge 4 commits from
feat/tasks-actions into dev
pull from: feat/tasks-actions
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: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:feat/tiering-761-a-migration
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!894
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/tasks-actions"
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?
Rework after review 76844 (recommendation c)
This rework follows the independent review 76844 (2 P1, 6 P2, 4 P3) and the plan in #808: every task note write goes through the one exact-byte executor that is already on dev (
crates/notes/src/write_executor/). Work is in two steps.Step 1a: shipped (gates and perf in the comments)
versionontasks.list/tasks.getrowstasks.movestatus,completed,scheduledand the owning checkbox lines on exact bytes, then the ranktasks.undo {job}(recorded inverse bytes), thentasks.reorder {restore}tasks.move-manyskippedbefore anything is writtentasks.undo {job}tasks.undo(new)tasks.reorderorder.db, a whole group the first timetasks.reorder {restore}tasks.update { title }notes.rename_taskjob, title sent alonenotes.undo {job}tasks.delete(1b)notes.restore {id}tasks.duplicate(1b)tasks.deleteof the copydelete.rs,duplicate.rs,lines.rsdeleted, actions removed from the contractstatus::plan_targetsplans both as one splice set;status_action::executesubmits one plan; a retry replays the recorded planreview_76844_p2_1_status_and_when_are_one_transactiontasks.undo= recorded inverse bytes; moves accept onlysomeday/todoreview_76844_p2_2_undo_restores_exact_bytes,undo_refuses_after_a_later_edit_and_writes_nothingreview_76844_p2_3_date_only_move_refuses_a_changed_notebetweencan loop0;betweenbounded and returnsNone; forward drags check the task, its neighbours andorderagainst the task index group (new optionaltoday/group_by/group_tag)review_76844_p2_5_bad_ranks_are_refused_and_between_always_ends,review_76844_p2_5_reorder_refuses_bad_ranks_and_other_groups,order_and_restore_name_each_task_oncereview_76844_p2_6_mixed_line_endings_refuse_and_keep_bytesstatus:keyNotes::rename_taskonly for ids the tasks index lists; other notes keep the H1 renamereview_76844_p3_rename_is_for_tasks_only_and_alonetitlemust be sent aloneDecisions
scheduled: 2026-10-20) are opaque leaves for the strict writer, which won't replace them. That move refuses with "edit in source mode" and keeps every byte (a_hand_typed_unquoted_date_refuses_and_keeps_bytes). Dates Calternal writes are quoted and move fine. Widening the strict grammar is a Calendar/Tasks decision."status": "todo"). This is existing B1 behavior on dev; undo restores the old spelling exactly.last edited.tzis still accepted. This matchestasks.set-status.Step 1b: shipped (interim delete + duplicate; gates 76937, perf 76971)
Delete = Trash the task note as an executor participant; leave its lines. The offline executor already places a removal as a journaled, recoverable rename that keeps the displaced inode (
recovery.rs::finish_inner). The Home layer refusespost: Nonetoday, so 1b adds a Trash participant there:room/executor.rs: imaging apost: Nonenote checks the pre-image and binding as today. The image is markedtrash(serde default, so old records still load).Payload::validate: a Trash image has no update, and its state equals the pre-state.published):Trash/<id>.mdis a hard link to the retained inode (exact bytes, no copy), plus<id>.json. Both steps are idempotent.links::rememberruns for the departed path.Cmd::Trash.deletedgoes into the samerecord_oncebatch.notes.restore {id}, the existing Trash restore job: it brings back the same id and the exact bytes. A strict executor inverse needs Home creation, which is step 2.Sizes: one participant, O(note bytes) to image; placement is O(1) renames.
ExpectedRoom+ full pre-imagePreparedpublished = falseDuplicate = new note only. Nothing existing is written, so it needs no executor participant:
status: todoand dropcompleted;tasks.create's path, with an id derived from the key.Undo is
notes.undoof that create job.Step 2: full version (after 1b)
Gates
Exact commands and results are posted as comments, never committed:
cargo fmt --all -- --checkcargo clippy -p calternal-tasks -p calternal-notes -p calternal-engine -p calternal --all-targets -- -D warningscargo test -p calternal-tasks,-p calternal-engine,-p calternal-notescargo test -p calternal contractandnode apps/web/scripts/action-types.mjs --checktasks-write-benchon a perf VM underperf-lock(gate:tasks.reorder_ackp95 ≤ 15 ms)Never merge.
🤖 Generated with Claude Code
Gates at
3928a30e(dev-01, Nix shell, through~/bin/slot)cargo fmt --all --checkcargo clippy --workspace --all-targets -j4 -- -D warningscargo test -j4 -p calternal-tasksactions_tests+reorder/tests)cargo test -j4 -p calternal-tasks -p calternal-notes -p calternal -p calternal-engine(run at the previous head54a92182, only clippy-only edits since)cargo test -j4 -p calternal contract_matches_the_servercontracts/actions.jsonregenerated: +6 actions,tasks.updategainstitle)node apps/web/scripts/action-types.mjs --checkactions.gen.tsregenerated, committed)New tests cover: row
versionequalsnotes.get's and a tick with it works with nonotes.get; rename (title, heading, file, links, undo, no H1 added, bad titles); reorder (one row, no file written, replay, group without ranks, stale undo refused); move (stale refused with bytes unchanged, only the date line changes, clear, undo, Someday ↔ day, placement + undo); move-many (per-item results, a stale item skipped, undo); delete (exact bytes, Trash, replay, undo byte for byte, odd lines left, last line without newline); duplicate (copy bytes, history cleared, line inserted, replay, undo, task with no line).Findings while testing (fixed in the PR): the status executor refuses when the room has unflushed bytes, so
tasks.movesaves the room first;last editedstamps on flush change the version, so the version handed back for Undo is read after the flush.Perf:
tasks.listwithversion, 10k notes + 10k tasks (perf-01, underperf-lock)tasks-bench --notes 10k --runs 3, release binaries built on dev-01 (cargo build --release --locked -p calternal-perf --bin tasks-bench), relayed through the control host, sha256 checked on both ends. base =origin/dev41f2efa3(51eb4a22…), branch =3928a30e(ce183533…). Alternated base, branch, base, branch in one session, same fixture Home. 200tasks.listcalls per list per run; the lists go through the real engine, so they include the version lookup.Cost of
version: about +0.2 to +0.35 ms at p50 (≈ +10–18 %) for a 200-row page (oneupdates_by_notelookup per row in one blocking call). Every list stays under 3.1 ms p95 here.GATE tasks.list,tasks.countsandtasks.describe: PASS in all four runs. These are the bench's own gates, so the task's "no regression beyond the gate" holds.Not caused by this PR, same on base:
GATE cold.index growth <= 10%FAILS in all four runs (base +19.8 % / +17.7 %, branch +18.9 % / +20.2 %: the tasks pass costs ~19 % of cold indexing, unchanged).GATE tasks.updateFAILED once on base (run 2) and once on branch (run 2) and passed the other two; it is flaky on this VM, with no code path of this PR in it.Not measured here: the new write actions (
move,reorder,delete,duplicate) have no bench yet; ADR 0066 liststasks.reorder_ack≤ 15 ms p95 with no file written, whichreorder_moves_one_row…asserts functionally (no file written) but does not time. Timing belongs on the perf VM in a follow-up.Raw outputs are kept on perf-01 at
~/perf/ta-keep/; the seeded Home and the binaries were removed.Independent review: #894 against the B1–B3 failure history (#798, #799, #801)
Verdict: (c) merge the best of both. Keep #894's API,
version, reorder and status-only move. Drop its delete, duplicate and line-scanner code (delete.rs,duplicate.rs,lines.rs), and rebuild those on the shared Home executor, using the planner rules and probes from #798 and #799. Then close #798 and #799. 2 P1, 6 P2, 4 P3.I reviewed head
3928a30eagainstorigin/dev41f2efa3, including every review comment on #798, #799 and #801. This is a code-reading review only. Nothing was built or run, because the control host is read-only and no VM probes were run, so the scenarios below are not reproduced. Items marked plausible depend on paths I read on dev but did not trace end to end.Key context. #798, #799 and #801 were put on hold for the same reason: #808 (
0066-tasks-writes-v2) says every task write must be a pure exact-splice planner run by ONE journaled executor. That executor and B1 v2 are already on dev (BM5: #809, #814, #812;crates/notes/src/write_executor/,docs/plans/tasks-v2-set-status.md). #894's status step uses that executor. Everything else in #894 (When edits, delete, duplicate, rename) goes through the old room andbulkpath. That is the path the v2 decision replaced, and where most of the earlier failures came from.1. The earlier failure cases, one by one
rename::edit_step→bulkStep::Edit→EditKind::Patch{base}.room/edit.rs:129–160forks the note atbaseand merges; it does not refuse. The When edit in move is a roomPropsedit. Only the status part goes through the executor.duplicate.rs:119–139inserts atb.endwith base = the snapshot version. If the source changed, the insert is merged in, with no check. See P1-1.delete.rs:54–60checks the version, then reads up to 64 source notes and waits for the job lock.delete.rs:87Step::Trash{note}carries no expected bytes.lines::blockremoves only the exact checkbox + single link-child shape. Anything else is left in place and counted.duplicate.rs:112). Every block ID is copied, not just ones on continuation lines. See P1-2.- [ ] sub ^id+- [Sub](<…>)), so each subtask gets a second owner. See P1-2.bulk/undo.rs::planplans only steps that have a result. Move and reorder undo is built only from changes that were actually made.notes.undoon a bulk job merges with later typing, and only the deleted-span guard runs. Move undo does refuse (it is version-checked) but is not exact. See P2-2.rename.rs:203) and the duplicate's properties use the lenientnotes::propswriter. Only the index'sbrokenflag refuses. B1 v2 uses the strict writer. To be fair,tasks.updateon dev already writesscheduledthis way.expectcheck, but a forward reorder never checks that the task belongs togroup, and rank values from the client are not checked. See P2-5.2. Findings
P1-1: delete and duplicate write to changed notes instead of refusing
The PR's failure table says "a note changed under a delete / copy plan → job fails, nothing deleted". The code doesn't do that.
delete.rs:68–86andduplicate.rs:119–139buildStep::Edit{base: snapshot.version}.room/edit.rs:129–160applies the Patch to a fork atbaseand merges it into the current text.- [ ] Call Bob ^aaaa…and its link child. A delete is planned. Before its step runs, a phone appendsbefore 5pmto that checkbox line (v11). The merged removal deletes the v10 characters and leavesbefore 5pmbehind as a stray fragment on the next line. The job reportsdone.Step::Trashhas no snapshot guard. If the task's status or description is edited after thedelete.rs:54check, the note is still trashed and the action reportsdone. Same as #798 r2 P1-1.P1-2: duplicate copies every block identity and every owning subtask line
duplicate.rs:99–112changes only six properties and keeps the body "exactly".- [ ] Draft outline ^aaaaaaaaaaaaaaaa, followed by the link child- [Outline](<Tasks/Outline.md>).^aaaa…, so a#^aaaa…link has two targets.review_r1_duplicate_multiline_block_id_is_freshandreview_r2_relationship_marker_retains_owner, are the starting point.P2-1: a move with both a When change and a status change is not atomic, and a replay after a crash can't finish it
moves.rs:186–200commits the status through the executor (key{key}:status). Thenmoves.rs:210–220does a separate roomPropsedit.todowith no date, so it shows in Anytime.one()now sees the status already matches, sochange_status = None. It falls tomoves.rs:202–208with the old version and returnsblock_changed. The half-done move is reported as a failure, and no undo is returned.move-many(moves.rs:313) has the same problem: items moved before the crash come backskipped, and their undo steps are lost.scheduledandcompletedas one exact-splice set for the task note, and submit it as one executor transaction.tasks::status::planalready composes property splices with the strict writer; add the When leaf. A replay then loads the recorded plan.P2-2: move's undo is not exact
moves.rs:231–237undoes by moving forward again ("move back").Move.statusaccepts all five statuses (moves.rs:54), even though the docs say someday/todo. Moving done → todo clearscompleted, and moving back to done stamps a newcompletedtime.scheduledis restored from the index value (now(),moves.rs:110–125), not from the raw bytes:tasks.reorder{restore}step are separate actions with no all-or-nothing rule.statusto someday/todo and store the rawscheduledandcompletedlines in the undo input.P2-3: a When-only move checks the version, then edits without a guard
moves.rs:202–220comparesnotes.get().version, then sends aPropsedit with no base. An edit that lands between the two is overwritten at the key level, on the lenient writer. It needs the same fix as P2-1.P2-4:
lines::blockgets the block's extent wronglines.rs:104–110looks only at line+2.P2-5:
tasks.reorderaccepts any rank and group from the clientrestore(reorder.rs:192–215) writes client-suppliedrankstrings with no charset or length check. Unknown bytes read as digit 0.0,between()(reorder.rs:246–268) never terminates."0"(withexpect= its current rank), then reorder Y withbefore: X.between("", Some("0"))pushes'0'forever.BEGIN IMMEDIATEtransaction on a blocking thread, and every other reorder for that Home waits.group.orderids are not checked to be tasks in that group, so ranks are stored for arbitrary strings.0;between;orderto be exactly the group's current members.P2-6: mixed line endings (plausible, same as #799 r1 P2)
Delete and duplicate source edits are saved through the room. A note with both LF and CRLF line endings comes back normalized, so untouched bytes change and undo can't restore them. Refuse such notes before writing, or let the executor's byte-exact placement handle it.
P3
rename.rs:52now switches to the task-title path for any note with astatus:key (for example a project note withstatus: active). Such a note gains atitle:key it never had. Use the task index instead (is this id an indexed task?).tasks.updatewith a title does the property edit and the rename as two steps (lib.rsupdate handler). If the rename fails, the first edit stays applied and no undo is returned.delete.rs:41–44) and returnsOk{state:"failed", undo:[…]}instead of the originalblock_changed.Delete{if_version}. If the copy was edited since, the delete refuses and the undo is left half done. That is safe, but it is reported as done.3. Access, idempotency, big-O
update::target) andnotes.enter(caller.user). A foreign id isnot_found. Shared-note permissions are correctly listed as out of scope.versioncosts oneMAX(seq)index lookup per row, and the perf comment shows about +0.3 ms at p50 for a 200-row page. Reorder is O(1) rows (O(G ≤ 2000) the first time). Delete and duplicate plan in O(source bytes), with at most 64 owners. The only unbounded cost is thebetweenloop in P2-5.4. Recommendation: (c), with the exact pieces
Keep from #894 (land once fixed):
versionon rows:versions.rs,History::versions, and its wiring intotasks.listandtasks.get;tasks.reorderandOrderDb::edit, with P2-5 fixed;tasks.moveandtasks.move-manyAPI and answer shapes;Rework in #894:
Drop from #894:
delete.rs,duplicate.rsandlines.rs, i.e. everything built onbulkStep::Edit/Trash.Rebuild delete and duplicate as v2 planners on the executor (#808's B3), using:
review_r1_*,review_r2_*andreview_r3_*probes as permanent regressions;review799_*probes.Interim, if the UI port can't wait for B3 v2:
Then close #798 and #799. Keep their branches until the probes are ported. #801 is already superseded by #812.
No code pushed, nothing merged.
Tasks write actions for the UI port: move, reorder, delete, duplicate, rename, version, bulk moveto Tasks write actions for the UI port: move, reorder, rename, version, bulk move. Delete and duplicate are being rebuilt on the executor.Gates: steps 1a + 1b on head
5f158246Run on
calternal-dev-01-compute02, in a Nix devshell, through~/bin/slot.cargo fmt --all -- --checkcargo clippy -j4 -p calternal-tasks -p calternal-notes -p calternal-engine -p calternal -p calternal-perf --all-targets -- -D warningsUPDATE_CONTRACTS=1 cargo test -j4 -p calternal contract(regenerated and committed in5f158246), thencargo test -j4 -p calternal contract_matches_the_servernode apps/web/scripts/action-types.mjs --checkcargo test -j4 -p calternal-taskscargo test -j4 -p calternal-enginecargo test -j4 -p calternal-notesNew probes for review 76844, all passing:
review_76844_p1_1_delete_refuses_a_changed_task,delete_trashes_through_the_executor_and_restores_exact_bytesreview_76844_p1_2_duplicate_rekeys_ids_and_refuses_subtasks,duplicate::tests::rekey_replaces_every_block_id_the_same_way_and_nothing_elsereview_76844_p2_1_status_and_when_are_one_transactionreview_76844_p2_2_undo_restores_exact_bytes,undo_refuses_after_a_later_edit_and_writes_nothingreview_76844_p2_3_date_only_move_refuses_a_changed_notereview_76844_p2_5_bad_ranks_are_refused_and_between_always_ends,review_76844_p2_5_reorder_refuses_bad_ranks_and_other_groups,order_and_restore_name_each_task_oncereview_76844_p2_6_mixed_line_endings_refuse_and_keep_bytesreview_76844_p3_rename_is_for_tasks_only_and_alonea_hand_typed_unquoted_date_refuses_and_keeps_bytesNot yet covered: a crash-point matrix for the new Trash participant (crash at Placed / Committed / HistoryInstalled / BatchPublished / PublicationRecorded, then recovery). The executor's own placement and recovery tests cover the removal itself. The Trash publication is idempotent by construction (link-or-verify plus details-if-missing), but no crash test drives it yet. It's the first item of step 2.
Perf: write actions on perf-01, under
perf-lockSetup: release binaries built on dev-01 at
cac156dd(cargo build --release --locked -p calternal-perf --bin tasks-write-bench), relayed through the control host. sha256 matched on both ends:82a5ce20…. The fixture is atasks-bench --notes 10kHome (10k notes + 10k task notes). Command:tasks-write-bench --base data/base-10000 --state state-w --n 200.Everything goes through the real action pipeline and is timed until the answer. Every move is undone, every delete restored, and every copy deleted.
tasks.reorder(one drag, 809-task group)tasks.moveWhen only (1 note)tasks.moveSomeday → day (status + When + owner line, 2 notes)tasks.undo(exact inverse)tasks.move-many50 taskstasks.move-many50 undotasks.set-status(B1, already on dev)tasks.delete(Trash through the executor)tasks.duplicate(new note only)Gates
tasks.reorder_ackp95 12.72 ms ≤ 15 ms (ADR 0066): PASS. First ranking of the group (ordersent): 34 ms.tasks.set-statuson dev, which uses the same shared executor and is slower than a move here. The cost is in the shared Home write executor, not in these adapters. Getting the executor near budget is its own perf task; I haven't changed the executor's speed in this PR.duplicatedoesn't touch the executor: p95 44 ms.Other findings
version: 0for a note never opened, andnotes.getopens it at a higher version. The client has to read the row again once. This was already the case before this PR.changed note inventory, while the index caught up after the 50-task move-many, and went through on retry. This is B1's existingExpectedInventoryguard. Under load, a client sees a spuriousblock_changedand must retry.The raw outputs (
write4.out, plus earlierwrite2.out) stay on perf-01 at~/perf/t894/. The seeded Home and state have been removed.Tasks write actions for the UI port: move, reorder, rename, version, bulk move. Delete and duplicate are being rebuilt on the executor.to Tasks write actions for the UI port: move, reorder, rename, version, bulk move, delete, duplicate (on the executor)Rebasing onto the executor hot-path fix (#898)
#898 (
perf/executor-hot-path, into dev) fixes the executor cost and the two spurious refusals this PR's bench found. Writes no longer reread the whole journal, the recovery sweep is skipped when nothing is in flight, and placement syncs are batched. No guard is weakened; the plan and failure table are in #898. Rebased onto #898, this branch passes the notes, tasks and engine tests. The bench numbers are in #898.A worked rebase of this PR's head
cac156ddis pushed asscratch/executor-on-894(never to be merged; it shows the resolution). To rebase:git rebase origin/perf/executor-hot-path(or onto dev once #898 lands). The conflicts are incrates/tasks/src/lib.rs(keep bothpub mod owners;andpub mod reorder;) andcrates/tasks/src/status_action.rs(keep yourexecute/plan_oneshape and apply the steps below).plan_one, replace the global epoch (home.note_write_inventory(),Requirement::ExpectedInventory { epoch }) withcrate::owners::read(&db.meta(), &folded)for each target that has owners. Plan from those rows. PushRequirement::ExpectedOwners { digest: crate::owners::digest(&owners) }, returncrate::owners::probe(owners.into_keys().collect())with the plan, and submit withhome.execute_note_write_probed(plan, Some(probe)). A move without owners gets the empty digest and always matches.versions::ofbecomeshome.row_versions(ids). Unopened notes get a negative file token instead of 0. Replace everyi.version != snap.versionorsnap.version != m.versionwith!home.version_matches(&id, sent, snap.version).await?(plan_one,moves.rs, andtrash_exactin delete).NoteWriteExecutor::read_recordsis gone. Incommitted_note_write, useNoteWriteExecutor::read_record(¬es, &journal, &home, &key), which reads one record file, not all of them.undo::inversestill takes the old epoch. The inverse's bytes are exact, so the probe has no owner rows to re-read; either drop the epoch or scope it to the inverse's notes. That's your call; #898 leaves it alone.🤖 Generated with Claude Code
kayg referenced this pull request2026-11-05 23:21:11 +00:00
Keeper proposal after reading #798, #799, #894 and all their review comments: gate #894 (
cac156dd68c222da099343628a75abfa97a75504), the replacement following review 76844, rather than revive the held bulk/room implementations. #798 round 3 explicitly requires stop-and-simplify; #799 is held for the same executor design. Propose closing #798/#799 as superseded only after recording the missing promotion/physical-line-move scope and preserving/porting their review probes. Do not close #894 as a duplicate: it carries the reviewed replacement API and the executor rework. Neither earlier PR is approved for merge. No PR closed or merged. I will merge current dev into #894, run fresh gates, add crash/retry regressions and report remaining scope explicitly.Crash/retry gate plan for current-dev qualification: keep one executor, no new write framework. Reproduced on
7cd0854aa: SIGKILL atPlaced(0)followed by the original Someday-to-day move returnsoperation key reused with different request or author. Cause: the fingerprint includes a derived target based on the current status, which recovery already changed. Fix72ef8de87fingerprints requested fields and enters replay even when the effective status is already satisfied. Cost remains O(request bytes), no new durable writes. New tests cover Placed/Committed/HistoryInstalled/BatchPublished/PublicationRecorded, exact inverse bytes, and one indexed task; delete covers the same five publication boundaries, plus a duplicate crash before the engine receipt.Per-field stale checks (#919) are an unresolved gate dependency: #928 is open and explicitly provides only an opt-in shared helper; Tasks does not yet opt in. No claim that whole-note versions satisfy this gate. The pending Tasks UI stack also still calls the old update/set-status actions, so its specs alone cannot cover move/reorder/delete. Added a browser-protocol regression spec on #894 for those actions, alongside the scratch UI integration matrix. No merge or close.
qualification update at head
2255752ccagainst dev02cdf87897c18486cd7856a487896019b3eb470d(still current):f4031f5a9, pushed72ef8de87andf0b98e5be; move/delete cover five executor boundariesbrowser spec explicitly covers protocol, not pending UI wiring: the UI stack still calls update/set-status. Notes intentionally excludes task rows, so its route is not a task editor. promotion and physical line moves remain missing scope compared with the older PRs. #894 is not GREEN. no merge or close.
reorder perf follow-up plan after the first locked RED result (p95 20.90 ms > 15 ms, 809-task group):
OrderDb::editcurrently opens a new SQLite connection, sets WAL, runs schema setup and closes it for every drag. keep one lazy writer connection per user, separate from the existing read-only connection. bound: one connection per already-open user; one write transaction still owns the whole rank plan and commit, with SQLite IMMEDIATE preserving cross-process exclusion. no schema or wire change. if open/setup fails, leave the cached slot empty and retry on the next action; if commit fails, return the error as before. validate with existing rank/restore/group tests and the same locked reorder fixture, then requalify Rust. #898 does not modify order.rs, so this does not duplicate its executor redesign.qualification continues at
57516bcbf(dev remains pinned to02cdf8789). fixed two original retry failures, then fixed the newly found legacy-record checksum regression in7b56b2ab6: omitting falseImage.trashpreserves version-one checksums. the existing ExpectedOwners compatibility tests were RED (2 failures) and now GREEN (3/3 including corrupt-record refusal). move/delete/duplicate recovery regression runner is GREEN (3 parent regressions across 11 kill windows, plus 2 helpers).fresh Chromium and WebKit action-protocol matrices on this head: 4/4 each at 1440/390. fmt, workspace/all-target clippy with warnings denied and targeted reorder tests pass. full workspace run is still active on dev-01 in
tasks-894-workspace-v3.service, log~/tasks-894-workspace-v3.log; the older workspace run was superseded and is not a pass.both shared perf locks were acquired in order at 05:53 IST and remain held. corrected tick ABAB (identical benchmark source built against dev and the PR), 100 accepted transitions per round, 50 counted retries, zero final refusals:
the initial tick mode without retries was discarded: it refused the actual transitions and mostly timed no-op resets.
b4e6a3f10fixes that harness bias. first reorder batch was RED at 20.90 ms p95;57516bcbfreuses the writer connection. the first fixed batch is GREEN at 5.41 ms for the same 809-task group; second batch pending.the first notes ABAB (3 warm runs per round) is not flat: sync.save_ack p95 +63.41%, storm.typing +6.32%, storm.save_ack -5.25%; save control -32.97%, typing control -0.14%. a 5-run-per-round repeat is queued using the final server binary; both results will be reported. final candidate server sha256
909e1c1c02df5f8306e60afa81e55dd8a073750e8bb1dddabef9008c7115b625; corrected final action benchmark9bce45e23d0ffe5f54b7c79d70cdd0bb5ebe9602b9023b344bc4be1dfbfac896.active perf-01 units:
tasks-894-perf-hold,tasks-894-reorder-fixed,tasks-894-notes-final; perf-02:tasks-894-perf-hold. tick comparison finished. reports are under~/perf/tasks-894/reports/. both holders will be stopped as soon as measurements finish. still BLOCKED on first-paint bytes (+81 raw/+31 gzip/+109 brotli) and per-field stale opt-in (#919/#928), independently of pending perf/workspace results. no merge or close.final gate report: blocked
head
bf08566987f9211d23920074d8f6694da1ee48d5, merged current dev258f3eac8a8fc925eeb5c57a9df2a4b68b44e365. no pr was merged or closed.the retry tests kill a real child process. move and delete cover placed(0), committed, history installed, batch published and publication recorded. delete checks exact trash bytes and restore. duplicate covers durable copy before receipt and checks one copy with stable marker identities. fixes include replay fingerprint stability, deterministic duplicate block ids, old journal checksum compatibility, and a lazy persistent reorder writer connection. each change was committed and pushed separately.
rust commands, through nix and ~/bin/slot on dev-01:
workspace v3 ran at
57516bcbf4165307cc9cf60518e21eebe4ba413c; its crates tree and cargo/flake files are identical to final head. fmt/clippy were repeated at final head. dev's old02cdf87897c18486cd7856a487896019b3eb470dand current258f3eac8also have identical rust/cargo/flake inputs. crates tree ids: dev8d3da72182449fb35b5d9c68e190a36541cbd195, candidate6734c40ad1e7acefe8e5c462a06922fd2d895ef7. the final merge adds frontend lazy autocomplete only. perf binaries were built before that merge and are backend-source-equivalent, not newly built at final head.browser commands, in apps/web under nix and ~/bin/slot, with CALTERNAL_BIN pointing to the worktree debug server:
frozen bun install, web check/unit tests and
bun run build && node scripts/bundle-size.mjs dist --jsonalso ran under nix/slot. current repository first-paint gate measures brotli. final dev/pr raw js 812596/812677, gzip 271284/271309, brotli 230948/230826. css raw/gzip/brotli 102659/21043/18383 on both.the pending ui stack was separately probed on scratch integration
9b54b7a8a, not the merge candidate: chromium 70 pass / 11 skip / 1 initial failure; webkit 69 / 11 / 2. composer and board failures did not reproduce in repeat-10 on either candidate or dev, so the brief's flake rule applies. tracked in #964 and #979. that stack still uses update/set-status; passing browser protocol cases does not claim move/reorder/delete ui wiring exists.perf: both shared locks held together, perf-01 acquired first; measurement on perf-01; fresh 10k-note/task fixtures, abab and dev-vs-dev control in the same session. all binaries verified by sha256. all jobs stopped, locks released and seeded data removed. raw reports, exact shell scripts and binary fingerprints are attached.
corrected tick (
--existing-only --n 50): each round has 100 accepted transitions, 50 retries and zero final refusals, including retry cost. p95 dev a1/a2 396.469/417.524 ms; candidate b1/b2 396.293/404.340 ms. means 406.996 → 400.317 ms, -1.64%; control 410.277 → 417.830 ms, +1.84%. old tick reports withoutv2timed mostly no-ops and are excluded from the verdict.new action endpoints are absent on dev, so these are absolute measurements, with delta n/a.
tasks-write-bench --n 50, two fresh fixtures:action timing is answered-attempt latency; logs separately report 100 version refetches and 50 late retries, zero final refusals. common tick above includes retry cost. before the persistent writer fix, reorder was 20.90/12.44 ms; fixed results pass twice.
final notes abab uses identical common
calternal-perfharness:--sizes 10k --runs 5 --cold-runs 1 --idle-secs 0 --storm-edits 10000 --storm-clients 4 --seed 1. p95 means of a1/a2 and b1/b2:earlier 3-warm-run results are retained separately, not pooled: sync.save_ack +63.41%, storm.typing +6.32%, storm.save_ack -5.25%. neither run establishes flat notes. #898 already owns executor hot-path redesign; this report does not merge or duplicate that work.
additional touched list/version path,
tasks-bench --notes 10000 --runs 3, 200 list samples and 2340 warm writes per round:all six list harness rounds exit 1 because first-touch update exceeds 40 ms and cold-index growth exceeds 10%, also on dev. these are complete measurement reports, not harness crashes or candidate-only failures.
keeper proposal: keep #894, which carries the reviewed executor replacement from review comment 76844. #798's round-three discussion explicitly requested simplification and #799 was held for the same redesign. conditionally close #798 (
f1600ed05109566241d1daa19c91be1d2b81ed1c) and #799 (7656894724bc5689f4ae3807665c703c455107bd) as superseded only after their review probes and missing promotion/physical-line-move scope have been preserved or explicitly resolved. do not close #894 as the duplicate. proposals were posted on all three; no closure or merge performed.evidence archive
Orchestrator status: WebKit specs are
pending: std VMs paused; no new jobs will start on dev-02 or the Mac while the pause is active. Chromium specs will run on dev-01. Perf work is waiting for the current perf-01 lock and the maintainer CPU pin; post-pin comparisons will use a fresh ABAB session and same-session dev-vs-dev control.Gate update: WebKit pending: std VMs paused. No dev-02 or Mac job has been started. Tasks and Notes Chromium is queued on dev-01 behind daily-train-1108; the fresh perf ABAB has not started while the train is active. I will post the post-pin comparison after it completes.
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.