Append cost no longer grows with the note (#527) #529

Closed
kayg wants to merge 0 commits from issue/527-append-fast into dev
Owner

Fixes #527. Found in #512 (comment 56705, perf-tails-2026-10-17.md).

Root cause

To append, the room copied the whole note (ydoc::fork(state)) only to learn where the text ends: Yjs puts new text after the last piece of text in the document, deleted or not, and yrs keeps that piece private. Building the copy re-integrates every piece, and yrs does this about quadratically when many writers chain off each other. Every keyed append is its own writer (the writer id comes from the idempotency key), so a log is exactly that shape. Measured on the copy alone: 0.1 ms at 100 lines, 2.4 ms at 1k, 15–26 ms at 3.5k.

Fix

The room remembers where its last append ended, plus the document's state vector at that moment (room/tail.rs). If the state vector is unchanged, the document has exactly the same pieces in the same order, so the next append is built directly as a one-item Yjs update (ydoc::append_update) and applied to the live doc. Deletes don't move the end. Anything that adds text (typing, outside edits, the server's keys at save) changes the state vector, and that one append takes the old copy path, which finds the end again.

What stays the same:

  • Text and merge behaviour. The applied change is byte-identical to the old path. A new fuzz test (40 seeds × 120 steps) checks this while typists insert, delete (often at the very end) and deliver late and out of order. A mutation check (dropping the state-vector guard) makes that test fail.
  • Crash safety and idempotency. Same key-derived writer, same "writer already in the doc" check, same history write before the answer.
  • History and versions. Same rows (the same applied bytes) and the same version numbers.

Patch and Replace are untouched.

Tests

  • tests/appends.rs:
    • byte-for-byte equivalence fuzz;
    • typists at the end plus keyed appends with retries in a real room (every word exactly once, clients converge, history replays to the note, file = note);
    • an append to a 2,000-line note stays small.
  • notes_crash.rs::kill_nine_mid_append_on_a_long_note: 4 concurrent writers append to a 1,000-line note, and the server gets 12 SIGKILLs. Unconfirmed appends are retried, and so are some confirmed ones. Each confirmed append is there exactly once, the file is never torn, and the file equals the note after a save.
  • examples/append_bench.rs: append p50/p95 at 100, 1k and 3.5k lines.

Bench (append_bench, durable keyed appends, local SSD, 3 alternating runs, 200 samples per size)

lines before p50 before p95 after p50 after p95
100 10.9–15.1 28.2–28.9 9.3–9.9 18.1–19.6
1,000 10.2–14.8 26.8–45.1 5.0–7.9 16.1–19.1
3,500 37.8–49.6 89.4–110.4 7.5–9.7 17.4–29.6

After the fix, appends cost the same at every size; what remains is the history fsync (~8–10 ms). The host was shared with other builds, so absolute numbers are noisy. The trend is not.

Gates (worktree, -j4)

  • cargo fmt --all --check: rc=0
  • cargo clippy --workspace --all-targets -- -D warnings: rc=0
  • cargo test --workspace: rc=0, 456 passed, 0 failed, 2 ignored (all notes_crash*, kill_nine_*, live-typing, writers and isolation tests included)

Follow-up (not in this PR)

save.rs::sync_title still forks base_state on every save to read the last saved text: the same quadratic copy, once per save rather than once per append. Caching that text would fix it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WCcFhwwgMK66k9LMVqogNJ

Fixes #527. Found in #512 (comment 56705, `perf-tails-2026-10-17.md`). ## Root cause To append, the room copied the whole note (`ydoc::fork(state)`) only to learn where the text ends: Yjs puts new text after the last piece of text in the document, deleted or not, and yrs keeps that piece private. Building the copy re-integrates every piece, and yrs does this about quadratically when many writers chain off each other. Every keyed append is its own writer (the writer id comes from the idempotency key), so a log is exactly that shape. Measured on the copy alone: 0.1 ms at 100 lines, 2.4 ms at 1k, 15–26 ms at 3.5k. ## Fix The room remembers where its last append ended, plus the document's state vector at that moment (`room/tail.rs`). If the state vector is unchanged, the document has exactly the same pieces in the same order, so the next append is built directly as a one-item Yjs update (`ydoc::append_update`) and applied to the live doc. Deletes don't move the end. Anything that adds text (typing, outside edits, the server's keys at save) changes the state vector, and that one append takes the old copy path, which finds the end again. What stays the same: - **Text and merge behaviour.** The applied change is byte-identical to the old path. A new fuzz test (40 seeds × 120 steps) checks this while typists insert, delete (often at the very end) and deliver late and out of order. A mutation check (dropping the state-vector guard) makes that test fail. - **Crash safety and idempotency.** Same key-derived writer, same "writer already in the doc" check, same history write before the answer. - **History and versions.** Same rows (the same applied bytes) and the same version numbers. Patch and Replace are untouched. ## Tests - `tests/appends.rs`: - byte-for-byte equivalence fuzz; - typists at the end plus keyed appends with retries in a real room (every word exactly once, clients converge, history replays to the note, file = note); - an append to a 2,000-line note stays small. - `notes_crash.rs::kill_nine_mid_append_on_a_long_note`: 4 concurrent writers append to a 1,000-line note, and the server gets 12 SIGKILLs. Unconfirmed appends are retried, and so are some confirmed ones. Each confirmed append is there exactly once, the file is never torn, and the file equals the note after a save. - `examples/append_bench.rs`: append p50/p95 at 100, 1k and 3.5k lines. ## Bench (`append_bench`, durable keyed appends, local SSD, 3 alternating runs, 200 samples per size) | lines | before p50 | before p95 | after p50 | after p95 | |---:|---:|---:|---:|---:| | 100 | 10.9–15.1 | 28.2–28.9 | 9.3–9.9 | 18.1–19.6 | | 1,000 | 10.2–14.8 | 26.8–45.1 | 5.0–7.9 | 16.1–19.1 | | 3,500 | 37.8–49.6 | 89.4–110.4 | 7.5–9.7 | 17.4–29.6 | After the fix, appends cost the same at every size; what remains is the history fsync (~8–10 ms). The host was shared with other builds, so absolute numbers are noisy. The trend is not. ## Gates (worktree, `-j4`) - `cargo fmt --all --check`: rc=0 - `cargo clippy --workspace --all-targets -- -D warnings`: rc=0 - `cargo test --workspace`: rc=0, 456 passed, 0 failed, 2 ignored (all `notes_crash*`, `kill_nine_*`, live-typing, writers and isolation tests included) ## Follow-up (not in this PR) `save.rs::sync_title` still forks `base_state` on every save to read the last saved text: the same quadratic copy, once per save rather than once per append. Caching that text would fix it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WCcFhwwgMK66k9LMVqogNJ
Append cost no longer grows with the note (#527)
Some checks failed
CI / server (pull_request) Successful in 35s
CI / rust (pull_request) Successful in 2m55s
CI / web (pull_request) Failing after 22s
71de5551a7
notes.append used to copy the whole note (ydoc::fork of its full state)
to find where the text ends, so every line added to a log made the next
append slower: the copy's Yjs integration is about quadratic in the
number of writers, and every keyed append is its own writer.

The room now remembers where its last append ended, together with the
document's state vector at that moment. While the state vector is
unchanged the document holds the very same pieces, so the next append is
built directly as a one-item Yjs update (ydoc::append_update). Anything
that adds text (typing, outside edits, server keys) changes the state
vector, and that one append makes the copy as before. The writer id
(idempotency key), history rows, versions and merge behaviour are
unchanged: tests check the applied change is byte-identical to the copy
path while typists insert, delete and deliver out of order.

- room/tail.rs: the remembered end and the append itself
- ydoc::append_update: the one-item update
- tests/appends.rs: byte-for-byte equivalence fuzz, typists + appends
  in a real room, long-note append stays small
- notes_crash.rs: kill -9 mid-append on a 1,000-line note, 4 writers
- examples/append_bench.rs: append p50/p95 at 100, 1k and 3.5k lines

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCcFhwwgMK66k9LMVqogNJ
Author
Owner

Independent review of #529: MERGE WITH CHANGES (small ones)

I found no way to lose, double or misplace text. There is one gap where the "byte-identical to the old path" claim is false: a third-party client that uses Y.Text formatting. The fix is a few lines. Reviewed 71de555 against origin/dev, with yrs 0.28.0 source.

What holds (checked in yrs and by tests)

  1. Deletes. TextRef::insert (yrs types/text.rs:212) walks to the index and then skips every deleted block to the right. So the old path always puts an append after the last item in the list, deleted or not. A delete changes only the delete set, never the list, so a cached last that has been deleted is still exactly where the old path would put the next append. Undo of an insert is a delete only (handled the same way). An "undelete" doesn't exist in Yjs: undo and restore make new items, so the state vector changes. GC turns deleted content into ContentDeleted but keeps the item and its IDs (it becomes a GC block only when the parent is deleted, and the root text never is). Squashing only merges blocks from the same client, and every append has its own writer.
  2. Origin, client and clock. The writer is always at clock 0. Keyed appends with clock > 0 return early in api_edit, and random writers start fresh. after_append only caches when the state vector grew by exactly writer: len, so last = writer:len-1 exists whenever seen == now. The update can therefore never be parked, and it is integrated just like the old one. No right origin is correct as long as the append is truly the last item; see finding 1 for the case where it isn't.
  3. Other root types / frontmatter map, outside-edit merge, tag rename, restore, server keys. All of these add items, so the state vector changes and the next append goes by copy once. Parked (pending) updates don't change the state vector. If prune_pending places them during our apply, after != expected and the cache is dropped.
  4. Concurrency. api_edit runs on &mut Room. Nothing awaits between reading the state vector, tail::append (synchronous: read, build, apply, update the cache) and commit_local. Remote updates are applied by the same actor, so they can't interleave. The cache lives only on Room, and the doc is never swapped in place (Awareness::new only in load.rs::new_room, which sets tail: None). Reload, rejoin and unload/reload all start cold.
  5. Crash and idempotency. Unchanged. The key → writer → clock_of > 0 check comes before this code. A cold cache means one copy. History append still happens before broadcast.

Tests I added and ran (review worktree, not pushed)

crates/notes/src/tests/appends_review.rs, cargo test -p calternal-notes --lib appends:

test tests::appends::appends_built_directly_match_the_copy_byte_for_byte ... ok
test tests::appends::typists_and_appends_interleave_on_one_note ... ok
test tests::appends::an_append_to_a_long_note_stays_small ... ok
test tests::appends_review::delete_whole_tail_then_append_matches ... ok
test tests::appends_review::frontmatter_map_write_falls_back ... ok
test tests::appends_review::fuzz_deletes_restarts_parked ... ok      (fast appends: 4003, all byte-identical)
test tests::appends_review::trailing_format_marker_bytes ... FAILED
  • delete_whole_tail_then_append_matches: five appends, then delete the last append, append, delete all text, append twice. Byte-identical every time.

  • frontmatter_map_write_falls_back: a write to another root map in the same doc. It falls back, then the fast path resumes, byte-identical.

  • fuzz_deletes_restarts_parked: 60 seeds × 200 steps, with:

    • typists inserting at the start or end and deleting trailing runs;
    • held updates delivered partially and out of order (so some are parked);
    • server-side deletes of the last 1–30 units, including across appended text;
    • restarts: both docs reloaded from their encoded state, cache cold;
    • appends with surrogate pairs.

    Every append was compared byte for byte with by_copy, and the snapshot and text were compared after every step. 4003 fast appends, no difference.

The PR's own fuzz already covers deletes at the tail, concurrent typing at the tail and out-of-order delivery. It doesn't cover restarts or other root types; mine adds those.

Finding 1: a trailing format marker breaks "byte-identical" (low severity, fix before merge)

Scenario: any client that writes Y.Text formatting at the end of md (insert_with_attributes / format) leaves a non-deleted closing Format(bold=null) item as the last item. First-party clients never do this (no .format/attributes on md in apps/, packages/), but any Yjs client that joins can.

  • The old path's find_position stops before that marker (it's not deleted, so it isn't skipped), so A1 goes before it, with right origin = the marker.
  • after_append still caches last = A1end, although A1 is no longer the last item.
  • The next append is built with origin A1end and no right origin, but the old path would set right origin = the marker.

Test output (trailing_format_marker_bytes):

A2 old=[1, 1, 233, 7, 0, 196, 232, 7, 2, 9, 11, 3, 65, 50, 10, 0]   // info 0xC4: origin + right origin 9:11
A2 new=[1, 1, 233, 7, 0, 132, 232, 7, 2, 3, 65, 50, 10, 0]          // info 0x84: origin only
byte-identical=false

Impact:

  • Text and snapshot are still identical after A2, since same() passed before the assert.
  • All replicas converge, because they all get the same bytes.
  • What differs is only the stored right origin, which can change how a future concurrent insert at that spot is ordered compared with the old path. So it's not data loss, but the PR's invariant ("same change, byte for byte") and the tail.rs doc ("right after the last piece of text, deleted or not") are both untrue in this case.

Fix (pick one):

  • (a) Only cache the tail when the appended item has no right neighbour, i.e. it really is the last item in the list. Check this after the apply, in tail::append, for both the copy path and the fast path.
  • (b) Cache the right origin too and write it in append_update (HAS_RIGHT_ORIGIN + write_right_id).

(a) is simpler and keeps the fast path for every first-party note. Also add trailing_format_marker_bytes (above) as a regression test.

Finding 2: doc wording (trivial)

tail.rs / ydoc::append_update say the end is "the last piece of text … deleted or not". It's precise only when no live non-text item (format marker) trails it. Reword once Finding 1 is fixed.

Not a blocker, worth knowing

Every append with a fresh writer adds a state vector entry forever. tail::append clones and compares the whole state vector each time, so the fast path is still O(#writers), not O(1). That's tiny next to a fork, but it does grow with the log.

Not run: notes_crash::kill_nine_mid_append_on_a_long_note (all build slots were busy with e2e). The crash and idempotency paths are unchanged by this diff (see 5).

## Independent review of #529: **MERGE WITH CHANGES** (small ones) I found no way to lose, double or misplace text. There is one gap where the "byte-identical to the old path" claim is false: a third-party client that uses Y.Text formatting. The fix is a few lines. Reviewed `71de555` against `origin/dev`, with yrs 0.28.0 source. ### What holds (checked in yrs and by tests) 1. **Deletes.** `TextRef::insert` (yrs `types/text.rs:212`) walks to the index and then skips **every deleted block to the right**. So the old path always puts an append after the last item in the list, deleted or not. A delete changes only the delete set, never the list, so a cached `last` that has been deleted is still exactly where the old path would put the next append. Undo of an insert is a delete only (handled the same way). An "undelete" doesn't exist in Yjs: undo and restore make new items, so the state vector changes. GC turns deleted content into `ContentDeleted` but keeps the item and its IDs (it becomes a `GC` block only when the parent is deleted, and the root text never is). Squashing only merges blocks from the same client, and every append has its own writer. 2. **Origin, client and clock.** The writer is always at clock 0. Keyed appends with clock > 0 return early in `api_edit`, and random writers start fresh. `after_append` only caches when the state vector grew by exactly `writer: len`, so `last = writer:len-1` exists whenever `seen == now`. The update can therefore never be parked, and it is integrated just like the old one. No right origin is correct as long as the append is truly the last item; see finding 1 for the case where it isn't. 3. **Other root types / frontmatter map, outside-edit merge, tag rename, restore, server keys.** All of these add items, so the state vector changes and the next append goes by copy once. Parked (pending) updates don't change the state vector. If `prune_pending` places them during our apply, `after != expected` and the cache is dropped. 4. **Concurrency.** `api_edit` runs on `&mut Room`. Nothing awaits between reading the state vector, `tail::append` (synchronous: read, build, apply, update the cache) and `commit_local`. Remote updates are applied by the same actor, so they can't interleave. The cache lives only on `Room`, and the doc is never swapped in place (`Awareness::new` only in `load.rs::new_room`, which sets `tail: None`). Reload, rejoin and unload/reload all start cold. 5. **Crash and idempotency.** Unchanged. The key → writer → `clock_of > 0` check comes before this code. A cold cache means one copy. History append still happens before broadcast. ### Tests I added and ran (review worktree, not pushed) `crates/notes/src/tests/appends_review.rs`, `cargo test -p calternal-notes --lib appends`: ``` test tests::appends::appends_built_directly_match_the_copy_byte_for_byte ... ok test tests::appends::typists_and_appends_interleave_on_one_note ... ok test tests::appends::an_append_to_a_long_note_stays_small ... ok test tests::appends_review::delete_whole_tail_then_append_matches ... ok test tests::appends_review::frontmatter_map_write_falls_back ... ok test tests::appends_review::fuzz_deletes_restarts_parked ... ok (fast appends: 4003, all byte-identical) test tests::appends_review::trailing_format_marker_bytes ... FAILED ``` - `delete_whole_tail_then_append_matches`: five appends, then delete the last append, append, delete **all** text, append twice. Byte-identical every time. - `frontmatter_map_write_falls_back`: a write to another root map in the same doc. It falls back, then the fast path resumes, byte-identical. - `fuzz_deletes_restarts_parked`: 60 seeds × 200 steps, with: - typists inserting at the start or end and deleting trailing runs; - held updates delivered partially and out of order (so some are parked); - server-side deletes of the last 1–30 units, including across appended text; - restarts: both docs reloaded from their encoded state, cache cold; - appends with surrogate pairs. Every append was compared byte for byte with `by_copy`, and the snapshot and text were compared after every step. 4003 fast appends, no difference. The PR's own fuzz already covers deletes at the tail, concurrent typing at the tail and out-of-order delivery. It doesn't cover restarts or other root types; mine adds those. ### Finding 1: a trailing format marker breaks "byte-identical" (low severity, fix before merge) **Scenario:** any client that writes Y.Text formatting at the end of `md` (`insert_with_attributes` / `format`) leaves a **non-deleted** closing `Format(bold=null)` item as the last item. First-party clients never do this (no `.format`/attributes on `md` in `apps/`, `packages/`), but any Yjs client that joins can. - The old path's `find_position` stops before that marker (it's not deleted, so it isn't skipped), so A1 goes *before* it, with right origin = the marker. - `after_append` still caches `last = A1end`, although A1 is no longer the last item. - The next append is built with origin A1end and **no right origin**, but the old path would set right origin = the marker. Test output (`trailing_format_marker_bytes`): ``` A2 old=[1, 1, 233, 7, 0, 196, 232, 7, 2, 9, 11, 3, 65, 50, 10, 0] // info 0xC4: origin + right origin 9:11 A2 new=[1, 1, 233, 7, 0, 132, 232, 7, 2, 3, 65, 50, 10, 0] // info 0x84: origin only byte-identical=false ``` **Impact:** - Text and snapshot are still identical after A2, since `same()` passed before the assert. - All replicas converge, because they all get the same bytes. - What differs is only the stored right origin, which can change how a *future concurrent* insert at that spot is ordered compared with the old path. So it's not data loss, but the PR's invariant ("same change, byte for byte") and the `tail.rs` doc ("right after the last piece of text, deleted or not") are both untrue in this case. **Fix (pick one):** - **(a)** Only cache the tail when the appended item has no right neighbour, i.e. it really is the last item in the list. Check this after the apply, in `tail::append`, for both the copy path and the fast path. - **(b)** Cache the right origin too and write it in `append_update` (`HAS_RIGHT_ORIGIN` + `write_right_id`). (a) is simpler and keeps the fast path for every first-party note. Also add `trailing_format_marker_bytes` (above) as a regression test. ### Finding 2: doc wording (trivial) `tail.rs` / `ydoc::append_update` say the end is "the last piece of text … deleted or not". It's precise only when no live non-text item (format marker) trails it. Reword once Finding 1 is fixed. ### Not a blocker, worth knowing Every append with a fresh writer adds a state vector entry forever. `tail::append` clones and compares the whole state vector each time, so the fast path is still O(#writers), not O(1). That's tiny next to a fork, but it does grow with the log. Not run: `notes_crash::kill_nine_mid_append_on_a_long_note` (all build slots were busy with e2e). The crash and idempotency paths are unchanged by this diff (see 5).
Author
Owner

Review addressed in the latest commit on issue/527-append-fast.

1. Trailing format mark. Fixed with option (a). tail::append now remembers the end only when the applied append has no right origin (ydoc::added_last reads the info byte of the one item in the applied update), so it really is the last item. If a format mark stays after it, the next append goes by copy again, byte-identical. Why no right origin means "last": TextRef::insert skips only deleted items to the right, so it gets no right origin only when nothing at all follows. The fast path itself never writes a right origin, and it only runs from such a remembered end with the state vector unchanged.

  • The doc comments in tail.rs and ydoc::append_update now say this precisely; "deleted or not" is gone.
  • All four of your tests are added as tests/appends_review.rs, with trailing_format_marker_bytes as the regression test. One clippy fix was needed: is_multiple_of.

2. Constant-cost check instead of the state-vector compare: not done. It wouldn't make an append O(1). yrs itself computes the full state vector on every append: TransactionMut::before_state, which encode_update_v1 and commit both need, plus the clock_of idempotency check. So the clone and compare are a constant factor on top of work yrs already does. A "length plus last writer's clock" check is not correct: a typist who keeps typing advances an existing writer's clock without adding an entry. An update counter driven by an observer would be correct, but it adds a subscription per room for no change in asymptotic cost. The compare stays.

Gates (worktree, build-slot, -j4, CARGO_TARGET_DIR=/home/kayg/Developer/calternal-target-527):

  • cargo fmt --all --check → fmt rc=0
  • cargo clippy --workspace --all-targets -- -D warnings → Finished \dev` profile [unoptimized + debuginfo] target(s)/clippy rc=0`
  • cargo test --workspace → test rc=0, 54 result lines all ok, 460 passed, 0 failed, 2 ignored, including:
test kill_nine_mid_append_on_a_long_note ... ok
test kill_nine_mid_tag_rename_renames_each_tag_once ... ok
test kill_nine_never_loses_or_doubles ... ok
test kill_nine_while_people_type_loses_and_doubles_nothing ... ok
test tests::appends_review::delete_whole_tail_then_append_matches ... ok
test tests::appends_review::frontmatter_map_write_falls_back ... ok
test tests::appends_review::trailing_format_marker_bytes ... ok
test tests::appends_review::fuzz_deletes_restarts_parked ... ok
test tests::appends::appends_built_directly_match_the_copy_byte_for_byte ... ok
test tests::appends::typists_and_appends_interleave_on_one_note ... ok
test tests::appends::an_append_to_a_long_note_stays_small ... ok

Not merged.

Review addressed in the latest commit on `issue/527-append-fast`. **1. Trailing format mark.** Fixed with option (a). `tail::append` now remembers the end only when the applied append has no right origin (`ydoc::added_last` reads the info byte of the one item in the applied update), so it really is the last item. If a format mark stays after it, the next append goes by copy again, byte-identical. Why no right origin means "last": `TextRef::insert` skips only deleted items to the right, so it gets no right origin only when nothing at all follows. The fast path itself never writes a right origin, and it only runs from such a remembered end with the state vector unchanged. - The doc comments in `tail.rs` and `ydoc::append_update` now say this precisely; "deleted or not" is gone. - All four of your tests are added as `tests/appends_review.rs`, with `trailing_format_marker_bytes` as the regression test. One clippy fix was needed: `is_multiple_of`. **2. Constant-cost check instead of the state-vector compare: not done.** It wouldn't make an append O(1). yrs itself computes the full state vector on every append: `TransactionMut::before_state`, which `encode_update_v1` and commit both need, plus the `clock_of` idempotency check. So the clone and compare are a constant factor on top of work yrs already does. A "length plus last writer's clock" check is not correct: a typist who keeps typing advances an existing writer's clock without adding an entry. An update counter driven by an observer would be correct, but it adds a subscription per room for no change in asymptotic cost. The compare stays. **Gates** (worktree, build-slot, `-j4`, `CARGO_TARGET_DIR=/home/kayg/Developer/calternal-target-527`): - `cargo fmt --all --check` → `fmt rc=0` - `cargo clippy --workspace --all-targets -- -D warnings` → `Finished \`dev\` profile [unoptimized + debuginfo] target(s)` / `clippy rc=0` - `cargo test --workspace` → `test rc=0`, 54 result lines all ok, **460 passed, 0 failed, 2 ignored**, including: ``` test kill_nine_mid_append_on_a_long_note ... ok test kill_nine_mid_tag_rename_renames_each_tag_once ... ok test kill_nine_never_loses_or_doubles ... ok test kill_nine_while_people_type_loses_and_doubles_nothing ... ok test tests::appends_review::delete_whole_tail_then_append_matches ... ok test tests::appends_review::frontmatter_map_write_falls_back ... ok test tests::appends_review::trailing_format_marker_bytes ... ok test tests::appends_review::fuzz_deletes_restarts_parked ... ok test tests::appends::appends_built_directly_match_the_copy_byte_for_byte ... ok test tests::appends::typists_and_appends_interleave_on_one_note ... ok test tests::appends::an_append_to_a_long_note_stays_small ... ok ``` Not merged.
Author
Owner

Landed in train y, #565.

Landed in train y, #565.
kayg closed this pull request 2026-10-17 20:10:14 +00:00
Some checks failed
CI / server (pull_request) Successful in 35s
CI / rust (pull_request) Successful in 2m55s
CI / web (pull_request) Failing after 22s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
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!529
No description provided.