Skip to content

fix(session): delete reverted messages boundary-last and tie-break ids by raw order - #42819

Open
NamedIdentity wants to merge 2 commits into
anomalyco:devfrom
NamedIdentity:fix/revert-cleanup-order-and-id-tiebreak
Open

fix(session): delete reverted messages boundary-last and tie-break ids by raw order#42819
NamedIdentity wants to merge 2 commits into
anomalyco:devfrom
NamedIdentity:fix/revert-cleanup-order-and-id-tiebreak

Conversation

@NamedIdentity

@NamedIdentity NamedIdentity commented Aug 15, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #42816

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Two fixes to the ID rollover ordering work.

1. Revert cleanup deletes the boundary message first.

SessionRevert.cleanup removes messages from the revert boundary onward. The boundary is remove[0] when no partID is set, and each removeMessage commits separately. If that loop is interrupted, the boundary is already gone but the rest are not. The next cleanup calls findIndex for the boundary, gets -1, so remove is empty and it deletes nothing. clearRevert() still runs at the end, discarding the marker that could have located those rows. They stay in the session permanently and get sent to the model on the next turn.

Using .toReversed() deletes the boundary last, so it works as a progress marker. An interrupted cleanup then leaves a state the next one can finish. The part loop below it has the same problem and the same fix.

2. The localeCompare tie-break does not match storage order.

Storage pages with ORDER BY time_created, id, and SQLite's default collation is BINARY. sync.tsx and Share.tsx tie-break the same data with localeCompare, which is locale collation. For two messages sharing a time.created these can return opposite results, so the client and the database can disagree about which comes first. Ids minted in the same millisecond by different processes share the timestamp prefix, so the random mixed case suffix decides, and that is exactly where the two collations differ.

localeCompare can also return 0 for two distinct strings that are canonically equivalent. MessageID only requires a msg prefix, so two different primary keys can compare equal and the sort becomes dependent on input order.

Raw < and > match what storage already does.

How did you verify your code works?

Read the affected paths and traced the interruption sequence above. Confirmed toReversed() is already used elsewhere in the repo, so there are no target or lib concerns.

Both fixes have been running in our fork for a day. We hit the same rollover in production on 2026-08-14, wrote the same core fix independently, then found yours had landed. These two turned up while auditing ours.

I have not run the full test suite against this branch.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Sean Smith added 2 commits August 15, 2026 14:41
Revert cleanup removed the boundary first. Each removal is a separate
durable transaction, so an interruption after the boundary left the
remaining reverted rows unreachable: the next cleanup's findIndex
returned -1, remove was empty, and clearRevert still discarded the only
marker that could have located them. Those rows then rejoined the
transcript as ordinary history and were sent to the model.

Iterating newest-first makes the boundary its own progress marker, so an
interrupted cleanup leaves a state the next one completes. The part loop
had the same shape and the same fix.
…ation

Message ordering tie-broke equal time.created with localeCompare, while
storage pages with ORDER BY time_created, id under SQLite's BINARY
collation. The two disagree: for ids differing only in suffix case,
en-US collation and raw byte order sort them oppositely, so a paginated
fetch and the client could order the same pair differently.

localeCompare also returns 0 for canonically-equivalent distinct
strings. MessageID only requires a msg prefix and is not restricted to
ASCII, so two distinct primary keys can compare equal and the sort
becomes input-order dependent.

Applies to the TUI message store and the shared transcript. The session
list sort is left alone -- it has no time component and feeds a binary
search whose relation would have to change with it.
@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. needs:title labels Aug 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Hey! Your PR title fix(session): delete reverted messages boundary-last and tie-break ids by raw order doesn't follow conventional commit format.

Please update it to start with one of:

  • feat: or feat(scope): new feature
  • fix: or fix(scope): bug fix
  • docs: or docs(scope): documentation changes
  • chore: or chore(scope): maintenance tasks
  • refactor: or refactor(scope): code refactoring
  • test: or test(scope): adding or updating tests

Where scope is the package name (e.g., app, desktop, opencode).

See CONTRIBUTING.md for details.

@NamedIdentity NamedIdentity changed the title fix(session): delete reverted messages boundary-last and tie-break ids by raw order fix(session): delete reverted messages boundary-last and tie-break ids by raw order Aug 15, 2026
@github-actions github-actions Bot removed needs:title needs:compliance This means the issue will auto-close after 2 hours. labels Aug 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Two ordering-correctness gaps remain after the ID-rollover fix

1 participant