Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions packages/opencode/src/session/revert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,13 @@ const layer = Layer.effect(
const index = msgs.findIndex((msg) => msg.info.id === messageID)
const target = index < 0 ? undefined : msgs[index]
const remove = index < 0 ? [] : msgs.slice(index + (session.revert.partID ? 1 : 0))
for (const msg of remove) {
// Newest-first, so the boundary message is removed last and acts as its own progress
// marker. Each removal is a separate durable transaction: if this loop is interrupted
// partway, the surviving boundary lets the next cleanup locate the remaining rows and
// finish. Deleting the boundary first would strand them -- the next findIndex returns
// -1, remove is empty, and clearRevert below still discards the only marker that could
// have found them, so they silently rejoin the transcript.
for (const msg of remove.toReversed()) {
yield* sessions.removeMessage({ sessionID, messageID: msg.info.id })
}
if (session.revert.partID && target) {
Expand All @@ -115,7 +121,9 @@ const layer = Layer.effect(
if (idx >= 0) {
const removeParts = target.parts.slice(idx)
target.parts = target.parts.slice(0, idx)
for (const part of removeParts) {
// Same reasoning as the message loop above: the boundary part is removed last so an
// interrupted cleanup stays resumable.
for (const part of removeParts.toReversed()) {
yield* sessions.removePart({ sessionID, messageID: target.info.id, partID: part.id })
}
}
Expand Down
6 changes: 5 additions & 1 deletion packages/tui/src/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ function search<T>(items: T[], target: string, key: (item: T) => string) {
}

function compareMessage(a: Message, b: Message) {
return a.time.created - b.time.created || a.id.localeCompare(b.id)
// Raw comparison, not localeCompare: storage pages with ORDER BY time_created, id under
// SQLite's BINARY collation, so locale collation can order a same-millisecond pair the
// opposite way here. localeCompare also returns 0 for canonically-equivalent distinct ids,
// which would make this sort input-order dependent.
return a.time.created - b.time.created || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
}

const messageKey = (message: Message) => message.time.created + message.id
Expand Down
6 changes: 5 additions & 1 deletion packages/web/src/components/Share.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,11 @@ export default function Share(props: {
messages: {},
})
const messages = createMemo(() =>
Object.values(store.messages).toSorted((a, b) => a.time.created - b.time.created || a.id.localeCompare(b.id)),
// Raw id comparison rather than localeCompare, matching the BINARY collation storage
// orders by. See the note on compareMessage in packages/tui/src/context/sync.tsx.
Object.values(store.messages).toSorted(
(a, b) => a.time.created - b.time.created || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
),
)
const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected"])

Expand Down
Loading