Skip to content

[lexical-website][lexical-y] Documentation Update: Check an agent's edit on the current state, then apply it there - #9688

Merged
zurfyx merged 1 commit into
mainfrom
zurfyx/y-agent-edit-check
Nov 1, 2026
Merged

zurfyx merged 1 commit into
mainfrom
zurfyx/y-agent-edit-check

Conversation

@zurfyx

@zurfyx zurfyx commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Description

y.md's "Edits made by code" told integrators to apply an agent's edit to
the state it was computed from and merge the result into the live
document. That always converges, but for an edit addressed by text or by
block id it converges to text nobody meant whenever the document changed
under the edit. Measured on @lexical/y with seedYState, editYState
and readYState (15 cases: a person's or a second agent's edit lands
between the agent's read and its edit):

Case Merging the edit made on the read Check, then apply to the current state
a collaborator replaced the same words The slowswift red fox refused (words changed)
a collaborator made the same change swiftswift refused
two agents, different words at risblocked refused
two agents, the same edit 4.2.2% refused
a collaborator typed elsewhere in the block right right
the words now also appear in another block right right (checked in the read's block)
the line was moved new text where the line was; the moved line keeps the old text right (block found by id)
words cut and pasted elsewhere The swift redfox jumps. refused
a block rewrite as a new node while a collaborator types in it their text moved to the start refused
a block delete while a collaborator types in it their sentence glued to the next line refused
a count outside the edited words wrong wrong (only the agent can catch it)

Merging was wrong in 10 of 15; the check was wrong in 1 and refused 2
edits that would have merged well. This is how text-matching edit tools
behave (Anthropic's str_replace and OpenAI's apply_patch match the
current text and fail on a miss; Claude Code refuses a write to a file
changed since its read; the Google Docs API's requiredRevisionId and
TipTap's server toolkit refuse a stale write so the caller re-reads).

Now:

  • y.md, "Edits made by code": Check what an edit names, then apply it to
    the current state
    : a replacement checks its own words in the block
    where it read them, found there by id (still there, exactly once); a
    rewrite or a delete checks the whole block (where it is, and its type,
    text, formats, style, NodeState and children, compared serialized, not
    only its text, as the checking schema declares them: run the check on a
    schema that still declares every field stored versions may hold, or a
    change to a dropped field goes unnoticed); a format or a comment on words
    the edit names checks those words like a replacement and is applied where
    they are found on the current state, never by offsets from the read; an
    insert checks its anchor block. Every edit of a batch
    is checked before any is applied, then again, in order, as it is applied
    on the state the earlier ones leave: an edit an earlier one changes (its
    words changed or now appear twice, its block deleted or rewritten, a block
    named twice) refuses the whole batch, a conservative rule. On a refusal,
    apply none, and return every failure (which edit, the reason, and the
    block's content in the latest stored state, not in the discarded edit;
    edits after an in-order failure are still checked) and a new read.
    On a pass, apply to the current state (blocks found by id there) and
    store with a compare-and-set; on a lost race, drop the state and the
    update computed on the older state and run the whole check and edit
    again on the newer one (bounded), or refuse back to the agent with a read
    of the state that won the last race: an update computed on an older state
    is never stored or sent. Merge from the read
    only where there is nothing to check
    (an append, a suggestion a person
    accepts, an edit by position): the fork-and-merge recipe stays for those
    cases.
  • y-server.md, "Edits by agents and tools" and "Edits by block id": the
    sketches check on the latest stored state and run editYState there,
    through applyChecked (the check; the edit, which checks each part again
    as it applies it and can refuse; the compare-and-set; up to 3 attempts,
    then a fresh load), instead of editing seen and merging. The word
    sketch takes a batch of replacements. $findWords finds a
    replacement's words by the block's id and requires them exactly once in
    that block's text, in one plain text node; $blockAt compares a target
    as a whole (its path and its serialized subtree); an answer by id is
    checked op by op and applied whole or not at all. "One edit at a time"
    says a checked edit starts over on a lost race instead of merging.
    ServerDocsAgentEdits.test.ts runs the sketches as written (they live in
    __tests__/utils/agentEditSketch.ts, and the first test checks that the
    page holds the same code), including targets changed since the read, a
    batch with one failing op, a batch whose first edit changes what the
    second (and third) names, refusals that report the stored content, a
    format on named words applied where they are now, and a store that loses
    the race.
  • block-ids.md, "Handles and collaboration": the handle is resolved and the
    whole block checked against what the model read on the stored state, and
    the edit is applied there by id with applyChecked; BlockHandles.test.ts
    follows it (a block the person typed in now goes back as changed).
  • DECISIONS.md: D94 (decided); D71 says D94 replaced which state the edit
    runs on.

Also, from the reviews of #9685:

Test plan

Before

The new cells against weaker sketches: no uniqueness check, a target
compared by its path and text only, and an update computed before a lost
race merged into the newer state (the docs' previous recipe):

$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/ServerDocsAgentEdits.test.ts
     × the sketches are the same in the docs and the tests 13ms
     × a replacement of words refuses words that appear twice in the block or across nodes, and finds the block read by id 10ms
     × step 3 refuses the whole answer when a target is gone, moved, replaced, typed in, formatted or styled since the read 42ms
     × a lost race starts over on the newer state: applied when the check still holds there, refused when not, never the older edit 20ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 4 ⎯⎯⎯⎯⎯⎯⎯
AssertionError: expected '// Your storage of one document: its …' to be '// Your storage of one document: its …' // Object.is equality
- Expected
+ Received
AssertionError: expected undefined to deeply equal [ { …(3) } ]
- Expected:
+ Received:
AssertionError: formatted: expected [] to deeply equal [ [ 1, 'the block changed' ] ]
- Expected
+ Received

An independent review of an earlier head of this branch composed the
word check per edit into a batch: each check passed on the stored state,
the first edit invalidated the second, and the batch was stored half done;
after 3 lost races the read returned was the one that had lost:

FAIL > dependent word batch refuses as a whole when an earlier op invalidates a later target
- "refused": true,  "sent": 0, "stores": 0, "text": "red fox\n\nspacer"
+ "refused": false, "sent": 1, "stores": 1, "text": "blue fox\n\nspacer"
FAIL > dependency that creates another occurrence cannot silently skip the second op
- "text": "red fox and hen\n\nspacer"
+ "text": "red fox and red\n\nspacer"
FAIL > retry exhaustion supplies a fresh read after the final known lost race
AssertionError: expected 'red fox\n\nxxspacer' to be 'red fox\n\nxxxspacer'

A second review found the refusal of such a batch reporting the block's
text from the discarded edit (blue fox and hen where storage held
red fox and hen), and only the first of two invalidated edits. Those cases
are cells of ServerDocsAgentEdits.test.ts now, as is a third review's
case, pinned as the documented scope: a field the server's schema dropped
is not compared, so a rewrite passes over a collaborator's change to it.

UndoInvisibleSteps.test.ts unpinned, on main:

$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/regressions/UndoInvisibleSteps.test.ts
 × undoing a format on text a collaborator deleted takes the step before it too (1,2)
 × undoing a format on text a collaborator deleted takes the step before it too (2,1)
 × undoing a comment removal on deleted text takes the format change before it too
AssertionError: expected 'one three X{0}[]' to be 'one three{0}[]'
      Tests  3 failed (3)

After

$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/ServerDocsAgentEdits.test.ts packages/lexical-y/src/__tests__/unit/BlockHandles.test.ts packages/lexical-y/src/__tests__/unit/regressions/UndoInvisibleSteps.test.ts packages/lexical-html/src/__tests__/unit/BlockIdDocs.test.ts
      Tests  30 passed | 3 expected fail (33)
$ LEXICAL_Y_GC=1 pnpm exec vitest run --project unit (same files)
      Tests  30 passed | 3 expected fail (33)
$ LEXICAL_Y_GC=1 pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit packages/lexical-html/src/__tests__/unit/BlockIdDocs.test.ts
 Test Files  225 passed | 2 skipped (227)
      Tests  18549 passed | 306 expected fail | 130 skipped (18985)
$ pnpm run ci-check
Found 0 errors

With gc off, the same lexical-y run had one failure, DisposeMemory.test.ts
("40 editors bound, edited and disposed in a synchronous loop hold no
document"), under 24 parallel workers; it passes 5 of 5 alone, and this
change touches no code it runs (docs and tests only).

@vercel

vercel Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
lexical Ready Ready Preview Nov 1, 2026 3:34am UTC
lexical-playground Ready Ready Preview Nov 1, 2026 3:34am UTC

Request Review

@zurfyx
zurfyx requested a review from etrepum as a code owner November 1, 2026 02:37
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Nov 1, 2026
@meta-code-review

meta-code-review Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 1, 2026, 3:52 AM UTC

@pkg-pr-new

pkg-pr-new Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
lexical

npm i https://pkg.pr.new/lexical@9688

@lexical/a11y

npm i https://pkg.pr.new/@lexical/a11y@9688

@lexical/clipboard

npm i https://pkg.pr.new/@lexical/clipboard@9688

@lexical/code

npm i https://pkg.pr.new/@lexical/code@9688

@lexical/code-core

npm i https://pkg.pr.new/@lexical/code-core@9688

@lexical/code-prism

npm i https://pkg.pr.new/@lexical/code-prism@9688

@lexical/code-shiki

npm i https://pkg.pr.new/@lexical/code-shiki@9688

@lexical/compiler

npm i https://pkg.pr.new/@lexical/compiler@9688

@lexical/devtools-core

npm i https://pkg.pr.new/@lexical/devtools-core@9688

@lexical/dragon

npm i https://pkg.pr.new/@lexical/dragon@9688

@lexical/eslint-plugin

npm i https://pkg.pr.new/@lexical/eslint-plugin@9688

@lexical/extension

npm i https://pkg.pr.new/@lexical/extension@9688

@lexical/fast-check

npm i https://pkg.pr.new/@lexical/fast-check@9688

@lexical/file

npm i https://pkg.pr.new/@lexical/file@9688

@lexical/hashtag

npm i https://pkg.pr.new/@lexical/hashtag@9688

@lexical/headless

npm i https://pkg.pr.new/@lexical/headless@9688

@lexical/history

npm i https://pkg.pr.new/@lexical/history@9688

@lexical/html

npm i https://pkg.pr.new/@lexical/html@9688

@lexical/internal

npm i https://pkg.pr.new/@lexical/internal@9688

@lexical/link

npm i https://pkg.pr.new/@lexical/link@9688

@lexical/list

npm i https://pkg.pr.new/@lexical/list@9688

@lexical/mark

npm i https://pkg.pr.new/@lexical/mark@9688

@lexical/markdown

npm i https://pkg.pr.new/@lexical/markdown@9688

@lexical/mdast

npm i https://pkg.pr.new/@lexical/mdast@9688

@lexical/offset

npm i https://pkg.pr.new/@lexical/offset@9688

@lexical/overflow

npm i https://pkg.pr.new/@lexical/overflow@9688

@lexical/plain-text

npm i https://pkg.pr.new/@lexical/plain-text@9688

@lexical/react

npm i https://pkg.pr.new/@lexical/react@9688

@lexical/rich-text

npm i https://pkg.pr.new/@lexical/rich-text@9688

@lexical/selection

npm i https://pkg.pr.new/@lexical/selection@9688

@lexical/table

npm i https://pkg.pr.new/@lexical/table@9688

@lexical/tailwind

npm i https://pkg.pr.new/@lexical/tailwind@9688

@lexical/text

npm i https://pkg.pr.new/@lexical/text@9688

@lexical/utils

npm i https://pkg.pr.new/@lexical/utils@9688

@lexical/y

npm i https://pkg.pr.new/@lexical/y@9688

@lexical/yjs

npm i https://pkg.pr.new/@lexical/yjs@9688

commit: 05d8b69

…dit on the current state, then apply it there

## Description

y.md's "Edits made by code" told integrators to apply an agent's edit to
the state it was computed from and merge the result into the live
document. That always converges, but for an edit addressed by text or by
block id it converges to text nobody meant whenever the document changed
under the edit. Measured on `@lexical/y` with `seedYState`, `editYState`
and `readYState` (15 cases: a person's or a second agent's edit lands
between the agent's read and its edit):

| Case | Merging the edit made on the read | Check, then apply to the current state |
| --- | --- | --- |
| a collaborator replaced the same words | `The slowswift red fox` | refused (words changed) |
| a collaborator made the same change | `swiftswift` | refused |
| two agents, different words | `at risblocked` | refused |
| two agents, the same edit | `4.2.2%` | refused |
| a collaborator typed elsewhere in the block | right | right |
| the words now also appear in another block | right | right (checked in the read's block) |
| the line was moved | new text where the line was; the moved line keeps the old text | right (block found by id) |
| words cut and pasted elsewhere | `The swift redfox jumps.` | refused |
| a block rewrite as a new node while a collaborator types in it | their text moved to the start | refused |
| a block delete while a collaborator types in it | their sentence glued to the next line | refused |
| a count outside the edited words | wrong | wrong (only the agent can catch it) |

Merging was wrong in 10 of 15; the check was wrong in 1 and refused 2
edits that would have merged well. This is how text-matching edit tools
behave (Anthropic's `str_replace` and OpenAI's `apply_patch` match the
current text and fail on a miss; Claude Code refuses a write to a file
changed since its read; the Google Docs API's `requiredRevisionId` and
TipTap's server toolkit refuse a stale write so the caller re-reads).

Now:

- y.md, "Edits made by code": **Check what an edit names, then apply it to
  the current state**: a replacement checks its own words in the block
  where it read them, found there by id (still there, exactly once); a
  rewrite or a delete checks the whole block (where it is, and its type,
  text, formats, style, NodeState and children, compared serialized, not
  only its text, as the checking schema declares them: run the check on a
  schema that still declares every field stored versions may hold, or a
  change to a dropped field goes unnoticed); a format or a comment on words
  the edit names checks those words like a replacement and is applied where
  they are found on the current state, never by offsets from the read; an
  insert checks its anchor block. Every edit of a batch
  is checked before any is applied, then again, in order, as it is applied
  on the state the earlier ones leave: an edit an earlier one changes (its
  words changed or now appear twice, its block deleted or rewritten, a block
  named twice) refuses the whole batch, a conservative rule. On a refusal,
  apply none, and return every failure (which edit, the reason, and the
  block's content in the latest stored state, not in the discarded edit;
  edits after an in-order failure are still checked) and a new read.
  On a pass, apply to the current state (blocks found by id there) and
  store with a compare-and-set; on a lost race, drop the state and the
  update computed on the older state and run the whole check and edit
  again on the newer one (bounded), or refuse back to the agent with a read
  of the state that won the last race: an update computed on an older state
  is never stored or sent. **Merge from the read
  only where there is nothing to check** (an append, a suggestion a person
  accepts, an edit by position): the fork-and-merge recipe stays for those
  cases.
- y-server.md, "Edits by agents and tools" and "Edits by block id": the
  sketches check on the latest stored state and run `editYState` there,
  through `applyChecked` (the check; the edit, which checks each part again
  as it applies it and can refuse; the compare-and-set; up to 3 attempts,
  then a fresh load), instead of editing `seen` and merging. The word
  sketch takes a batch of replacements. `$findWords` finds a
  replacement's words by the block's id and requires them exactly once in
  that block's text, in one plain text node; `$blockAt` compares a target
  as a whole (its path and its serialized subtree); an answer by id is
  checked op by op and applied whole or not at all. "One edit at a time"
  says a checked edit starts over on a lost race instead of merging.
  `ServerDocsAgentEdits.test.ts` runs the sketches as written (they live in
  `__tests__/utils/agentEditSketch.ts`, and the first test checks that the
  page holds the same code), including targets changed since the read, a
  batch with one failing op, a batch whose first edit changes what the
  second (and third) names, refusals that report the stored content, a
  format on named words applied where they are now, and a store that loses
  the race.
- block-ids.md, "Handles and collaboration": the handle is resolved and the
  whole block checked against what the model read on the stored state, and
  the edit is applied there by id with `applyChecked`; `BlockHandles.test.ts`
  follows it (a block the person typed in now goes back as `changed`).
- DECISIONS.md: D94 (decided); D71 says D94 replaced which state the edit
  runs on.

Also, from the reviews of #9685:

- y.md, Servers: the per-client order paragraph now opens "Causal order
  includes each client's own order", and the json-joy comparison is
  dropped (y.md and D92).
- D93 and y.md name the orders: one order when the comment ends at the
  rewritten word, both when it continues past it.
- `regressions/UndoInvisibleSteps.test.ts` (from #9630) pins D81: the 3
  cells that assert a press also taking the next step are `test.fails`.
  They fail on main with gc off and on.

## Test plan

### Before

The new cells against weaker sketches: no uniqueness check, a target
compared by its path and text only, and an update computed before a lost
race merged into the newer state (the docs' previous recipe):

```
$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/ServerDocsAgentEdits.test.ts
     × the sketches are the same in the docs and the tests 13ms
     × a replacement of words refuses words that appear twice in the block or across nodes, and finds the block read by id 10ms
     × step 3 refuses the whole answer when a target is gone, moved, replaced, typed in, formatted or styled since the read 42ms
     × a lost race starts over on the newer state: applied when the check still holds there, refused when not, never the older edit 20ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 4 ⎯⎯⎯⎯⎯⎯⎯
AssertionError: expected '// Your storage of one document: its …' to be '// Your storage of one document: its …' // Object.is equality
- Expected
+ Received
AssertionError: expected undefined to deeply equal [ { …(3) } ]
- Expected:
+ Received:
AssertionError: formatted: expected [] to deeply equal [ [ 1, 'the block changed' ] ]
- Expected
+ Received
```

An independent review of an earlier head of this branch composed the
word check per edit into a batch: each check passed on the stored state,
the first edit invalidated the second, and the batch was stored half done;
after 3 lost races the read returned was the one that had lost:

```
FAIL > dependent word batch refuses as a whole when an earlier op invalidates a later target
- "refused": true,  "sent": 0, "stores": 0, "text": "red fox\n\nspacer"
+ "refused": false, "sent": 1, "stores": 1, "text": "blue fox\n\nspacer"
FAIL > dependency that creates another occurrence cannot silently skip the second op
- "text": "red fox and hen\n\nspacer"
+ "text": "red fox and red\n\nspacer"
FAIL > retry exhaustion supplies a fresh read after the final known lost race
AssertionError: expected 'red fox\n\nxxspacer' to be 'red fox\n\nxxxspacer'
```

A second review found the refusal of such a batch reporting the block's
text from the discarded edit (`blue fox and hen` where storage held
`red fox and hen`), and only the first of two invalidated edits. Those cases
are cells of `ServerDocsAgentEdits.test.ts` now, as is a third review's
case, pinned as the documented scope: a field the server's schema dropped
is not compared, so a rewrite passes over a collaborator's change to it.

`UndoInvisibleSteps.test.ts` unpinned, on main:

```
$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/regressions/UndoInvisibleSteps.test.ts
 × undoing a format on text a collaborator deleted takes the step before it too (1,2)
 × undoing a format on text a collaborator deleted takes the step before it too (2,1)
 × undoing a comment removal on deleted text takes the format change before it too
AssertionError: expected 'one three X{0}[]' to be 'one three{0}[]'
      Tests  3 failed (3)
```

### After

```
$ pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit/ServerDocsAgentEdits.test.ts packages/lexical-y/src/__tests__/unit/BlockHandles.test.ts packages/lexical-y/src/__tests__/unit/regressions/UndoInvisibleSteps.test.ts packages/lexical-html/src/__tests__/unit/BlockIdDocs.test.ts
      Tests  30 passed | 3 expected fail (33)
$ LEXICAL_Y_GC=1 pnpm exec vitest run --project unit (same files)
      Tests  30 passed | 3 expected fail (33)
$ LEXICAL_Y_GC=1 pnpm exec vitest run --project unit packages/lexical-y/src/__tests__/unit packages/lexical-html/src/__tests__/unit/BlockIdDocs.test.ts
 Test Files  225 passed | 2 skipped (227)
      Tests  18549 passed | 306 expected fail | 130 skipped (18985)
$ pnpm run ci-check
Found 0 errors
```

With gc off, the same lexical-y run had one failure, `DisposeMemory.test.ts`
("40 editors bound, edited and disposed in a synchronous loop hold no
document"), under 24 parallel workers; it passes 5 of 5 alone, and this
change touches no code it runs (docs and tests only).

@meta-code-review meta-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks, this looks good. The checked recipe reads correctly, the sketches on the server page match agentEditSketch.ts (the first test enforces it), and the changed tests pass with gc on and off. Everything below is optional and non-blocking.

  1. Discarded updates move the agent's writer to a new client id (packages/lexical-website/docs/collaboration/y-server.md:445 and :451). editYState commits the writer's position when it returns. So once applyChecked drops an update (an in-order refusal or a lost race), the next edit on the stored state writes under a new client id and raises a writer-new-client-id warning, whose text asks callers to store every update it returns. A refused dependent batch followed by a passing edit takes the document from 2 to 3 client ids. For the in-order pass, throw from inside $edit and catch it around editYState: the writer stays on its id. For a lost race, "Give the agent its writer" (y-server.md:375-379) could name the cost; D45 in packages/lexical-y/DECISIONS.md is the rule about one client id per writer.
  2. Updates already in flight still merge (y-server.md:565-567, and y.md's "Check what an edit names"). The compare-and-set covers stored states only. A collaborator's update made before the agent's reached them is merged afterwards: on The quick fox., the agent's checked quick→slow is stored, then their quick→swift arrives and the line reads slowswift. The previous page said this ("one already on its way"); one sentence would keep it.
  3. Overlapping replacements in one batch pass (y-server.md:477). The in-order recheck only asks whether the words still occur once. So on red fox, [red fox→fox red, red→blue] stores fox blue, and the second edit lands on the first edit's new word. Refusing edits whose ranges overlap in one block during the initial check would match the rule at y-server.md:339-341 and D94.
  4. Block IDs sketch (packages/lexical-website/docs/extensions/block-ids.md:429-438 and :450). read holds only the root's children while shown holds every block's handle, so a list item's handle gets changed on an unchanged document every time. Also, [result.status] drops the candidates and reason that the status table says to send back. Building read over every id in shown and returning [result] fixes both.
  5. D94 cites a 15-case measurement that exists only in the PR description. packages/lexical-y/AGENTS.md asks for a repro or measurement before a DECIDED row is reopened, so committing those cases as a test (the merge result beside the check result) would let the row be re-checked later.

},
{writer: agent},
);
if (failed.length > 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: by the time editYState returns, it has committed this update to the writer. Dropping it here (and at the lost race below) makes the next edit write under a new client id, with a writer-new-client-id warning. To keep the writer on its id, throw from inside $edit on an in-order failure and catch it around editYState. The lost-race cost could be named in "Give the agent its writer".

if (from === '' || at === -1) {
return 'the words changed';
}
if (all.indexOf(from, at + 1) !== -1) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: this only asks whether the words still occur once. An earlier edit of the batch that rewrites them into text that still contains them once (red fox→fox red, then red→blue) lets the later edit land on the new word (fox blue). Refusing overlapping ranges in the initial check would match the rule at lines 339-341.

schema that still declares every field the stored versions may hold. Store
the result with `applyChecked` (above): a compare-and-set on the version
the check read, and on a lost race the check and the edit again on the
newer state, so that no change lands between the check and the edit.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: the compare-and-set covers stored states only. A collaborator's update made before the agent's reached them still merges afterwards: on The quick fox., the agent's quick→slow is stored, then their quick→swift gives slowswift. The previous page said so ("one already on its way"); one sentence would keep that.

// shown with it.
// shown with it, and each block it read as a whole.
const shown = readYState(seen, config, () => $getBlockHandles());
const read = new Map(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: read holds only the root's children, while shown has every block's handle, so a list item's handle gets changed on an unchanged document every time. Build read from every id in shown, and return [result] at line 450 so ambiguous keeps its candidates; that fixes both.

@zurfyx
zurfyx added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit f31e23a Nov 1, 2026
57 checks passed
zurfyx added a commit that referenced this pull request Nov 1, 2026
…ome guards, D95 moot, gc-on #9623 cells pinned, D51 back to an accepted limit

## Description

Rebased on `main` 064b18e (#9688, #9691). Without #9679's rules:

- The #9690 cells (`FormatRuleTrade.test.ts`, `FormatRuleTradeSessions.test.ts`)
  pass: they become plain tests, guards against bringing those rules back as
  they were. D95 is marked moot (`REVERTED`).
- `UndoRestoredRunOverDeletedText.test.ts`: with gc on, the sequential order
  of six shapes spreads again (the undo cannot find items gc collected):
  `it.fails` with gc on, titled "(#9623 with gc on)". The #9689 cells are
  unchanged.
- `UndoInvisibleSteps.test.ts` (#9688): the cell that needed a delete to keep
  the comment removal's null passes again with the collapse; it becomes a
  plain test, with its comment updated.
- D51 goes back to `ACCEPTED LIMIT` (its status before #9679), keeping the
  D86 sentence and the revert note.

## Test plan

### Before

On 064b18e with the revert alone, `FormatRuleTrade*` and one
`UndoInvisibleSteps` cell failed as unexpected passes, and twelve gc-on cells
of `UndoRestoredRunOverDeletedText` failed.

### After

```
$ LEXICAL_Y_GC=0 vitest --run --project unit packages/lexical-y/src/__tests__/unit/regressions/{UndoInvisibleSteps,UndoRestoredRunOverDeletedText,FormatRuleTrade,FormatRuleTradeSessions}.test.ts
      Tests  35 passed | 46 expected fail (81)
$ LEXICAL_Y_GC=1 (same files)
      Tests  35 passed | 46 expected fail (81)
```

This branch was successfully deployed

2 active deployments
Preview – lexical — 05d8b696 Deployed Nov 1, 2026 by vercel[bot]
Preview – lexical-playground — 05d8b696 Deployed Nov 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant