Compare commits

...

1 Commits

Author SHA1 Message Date
Dax Raad 1c583dae3c fix: make revert boundaries chronological 2026-08-14 01:01:33 +00:00
5 changed files with 26 additions and 18 deletions
@@ -480,23 +480,23 @@ describe("server session", () => {
test("projects committed revert before server reconciliation", () => {
const ctx = setup({ child: session("child") })
ctx.store.remember({ ...session("child"), revert: { messageID: "msg_2", partID: "prt_1" } })
ctx.store.set("input", "child", ["msg_1", "msg_2"])
ctx.store.remember({ ...session("child"), revert: { messageID: "msg_000", partID: "prt_1" } })
ctx.store.set("input", "child", ["msg_fff", "msg_000"])
ctx.store.set("session_message", "child", [
{ id: "msg_1", type: "user", text: "keep", time: { created: 1 } },
{ id: "msg_2", type: "user", text: "remove", time: { created: 2 } },
{ id: "msg_fff", type: "user", text: "keep", time: { created: 1 } },
{ id: "msg_000", type: "user", text: "remove", time: { created: 2 } },
])
ctx.store.applyV2({
id: "evt_revert",
created: 3,
type: "session.revert.committed",
data: { sessionID: "child", to: "msg_2" },
data: { sessionID: "child", to: "msg_000" },
} as OpenCodeEvent)
expect(ctx.store.data.info.child?.revert).toBeUndefined()
expect(ctx.store.data.input.child).toEqual(["msg_1"])
expect(ctx.store.data.session_message.child?.map((message) => message.id)).toEqual(["msg_1"])
expect(ctx.store.data.input.child).toEqual(["msg_fff"])
expect(ctx.store.data.session_message.child?.map((message) => message.id)).toEqual(["msg_fff"])
})
test("does not restore a message hydrated before a committed revert", async () => {
+7 -3
View File
@@ -1041,13 +1041,17 @@ export function createServerSession(
if (event.type === "session.revert.committed") {
messageHydrationRevision.set(sessionID, (messageHydrationRevision.get(sessionID) ?? 0) + 1)
if (info) remember({ ...info, revert: undefined })
setData("input", sessionID, (items) => items?.filter((id) => id < event.data.to))
setData("input", sessionID, (items) => {
const boundary = items?.findIndex((id) => id === event.data.to) ?? -1
return boundary < 0 ? items : items?.slice(0, boundary)
})
const source = data.session_message[sessionID] ?? []
const removed = source.filter((message) => message.id >= event.data.to).map((message) => message.id)
const boundary = source.findIndex((message) => message.id === event.data.to)
const removed = boundary < 0 ? [] : source.slice(boundary).map((message) => message.id)
removedMessages.set(sessionID, new Set([...(removedMessages.get(sessionID) ?? []), ...removed]))
projectV2({
sessionID,
messages: source.filter((message) => message.id < event.data.to),
messages: boundary < 0 ? source : source.slice(0, boundary),
touched: [],
removed,
})
+5 -2
View File
@@ -833,10 +833,13 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
"session",
"input",
event.data.sessionID,
(store.session.input[event.data.sessionID] ?? []).filter((id) => id < event.data.to),
(items) => {
const boundary = items?.findIndex((id) => id === event.data.to) ?? -1
return boundary < 0 ? items : items?.slice(0, boundary)
},
)
message.update(event.data.sessionID, (draft, index) => {
const position = draft.findIndex((item) => item.id >= event.data.to)
const position = draft.findIndex((item) => item.id === event.data.to)
if (position === -1) return
for (const item of draft.splice(position)) index.delete(item.id)
})
+2 -1
View File
@@ -52,8 +52,9 @@ export function createSessionRows(sessionID: Accessor<string>) {
)
const visible = queued.size === 0 ? messages : messages.filter((message) => !queued.has(message.id))
const boundary = revertBoundary()
const boundaryIndex = boundary ? visible.findIndex((message) => message.id === boundary) : -1
const rows = reduceSessionRows(
boundary ? visible.filter((message) => message.id < boundary) : visible,
boundaryIndex < 0 ? visible : visible.slice(0, boundaryIndex),
inputs,
turnTokens(),
)
+5 -5
View File
@@ -1105,7 +1105,7 @@ test("removes committed revert messages from local state", async () => {
))
try {
for (const [seq, inboxID] of ["msg_001", "msg_002", "msg_003"].entries()) {
for (const [seq, inboxID] of ["msg_fff", "msg_000", "msg_001"].entries()) {
emitEvent(events, {
id: Event.ID.create(),
created: seq,
@@ -1121,13 +1121,13 @@ test("removes committed revert messages from local state", async () => {
created: 3,
type: "session.revert.committed",
durable: durable(sessionID, 3),
data: { sessionID, to: "msg_002" },
data: { sessionID, to: "msg_000" },
})
await wait(() => data.session.message.list(sessionID).length === 1)
expect(data.session.message.list(sessionID).map((message) => message.id)).toEqual(["msg_001"])
expect(data.session.message.get(sessionID, "msg_002")).toBeUndefined()
expect(data.session.message.get(sessionID, "msg_003")).toBeUndefined()
expect(data.session.message.list(sessionID).map((message) => message.id)).toEqual(["msg_fff"])
expect(data.session.message.get(sessionID, "msg_000")).toBeUndefined()
expect(data.session.message.get(sessionID, "msg_001")).toBeUndefined()
} finally {
app.renderer.destroy()
}