diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index 79fef2cda..03e5afd08 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -71,7 +71,8 @@ const layer = Layer.effect( if (session.revert?.snapshot) yield* snap.restore(session.revert.snapshot) yield* snap.revert(patches) if (rev.snapshot) rev.diff = yield* snap.diff(rev.snapshot) - const range = all.filter((msg) => msg.info.id >= rev.messageID) + const index = all.findIndex((msg) => msg.info.id === rev.messageID) + const range = index < 0 ? [] : all.slice(index) const diffs = yield* summary.computeDiff({ messages: range }) yield* storage.write(["session_diff", input.sessionID], diffs).pipe(Effect.ignore) yield* events.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs }) @@ -102,20 +103,9 @@ const layer = Layer.effect( const sessionID = session.id const msgs = yield* sessions.messages({ sessionID }).pipe(Effect.orDie) const messageID = session.revert.messageID - const remove = [] as SessionV1.WithParts[] - let target: SessionV1.WithParts | undefined - for (const msg of msgs) { - if (msg.info.id < messageID) continue - if (msg.info.id > messageID) { - remove.push(msg) - continue - } - if (session.revert.partID) { - target = msg - continue - } - remove.push(msg) - } + 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) { yield* sessions.removeMessage({ sessionID, messageID: msg.info.id }) } diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index de8c3dc4c..cfe034146 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -703,9 +703,9 @@ const layer: Layer.Layer< }) const msgs = yield* messages({ sessionID: input.sessionID }) const idMap = new Map() + const target = input.messageID ? msgs.findIndex((msg) => msg.info.id === input.messageID) : msgs.length - for (const msg of msgs) { - if (input.messageID && msg.info.id >= input.messageID) break + for (const msg of msgs.slice(0, target < 0 ? msgs.length : target)) { const newID = MessageID.ascending() idMap.set(msg.info.id, newID) diff --git a/packages/opencode/test/session/revert-compact.test.ts b/packages/opencode/test/session/revert-compact.test.ts index d3d7ba7aa..f9e8cfd9a 100644 --- a/packages/opencode/test/session/revert-compact.test.ts +++ b/packages/opencode/test/session/revert-compact.test.ts @@ -35,6 +35,18 @@ const user = Effect.fn("test.user")(function* (sessionID: SessionID, agent = "de }) }) +const userAt = Effect.fn("test.userAt")(function* (sessionID: SessionID, id: string, created: number) { + const session = yield* Session.Service + return yield* session.updateMessage({ + id: MessageID.make(id), + role: "user" as const, + sessionID, + agent: "default", + model: { providerID: ProviderV2.ID.make("openai"), modelID: ModelV2.ID.make("gpt-4") }, + time: { created }, + }) +}) + const assistant = Effect.fn("test.assistant")(function* (sessionID: SessionID, parentID: MessageID, dir: string) { const session = yield* Session.Service return yield* session.updateMessage({ @@ -426,6 +438,39 @@ describe("revert + compact workflow", () => { ), ) + it.live( + "reverts chronological suffixes on both sides of mixed message ID ordering", + provideTmpdirInstance( + () => + Effect.gen(function* () { + const session = yield* Session.Service + const revert = yield* SessionRevert.Service + const ids = ["msg_z9-before", "msg_z1-before-wrap", "msg_a0-after-wrap", "msg_a1-after"] + + const run = Effect.fn("test.mixedIDRevert")(function* (target: number) { + const info = yield* session.create({}) + for (const [index, id] of ids.entries()) { + const message = yield* userAt(info.id, id, index + 1) + yield* text(info.id, message.id, id) + } + + const reverted = yield* revert.revert({ + sessionID: info.id, + messageID: MessageID.make(ids[target]!), + }) + yield* revert.cleanup(reverted) + const remaining = yield* session.messages({ sessionID: info.id }) + yield* session.remove(info.id) + return remaining.map((msg) => msg.info.time.created) + }) + + expect(yield* run(1)).toEqual([1]) + expect(yield* run(2)).toEqual([1, 2]) + }), + { git: true }, + ), + ) + it.live( "cleanup is a no-op when session has no revert state", provideTmpdirInstance( diff --git a/packages/opencode/test/session/session.test.ts b/packages/opencode/test/session/session.test.ts index d10918198..c4301d204 100644 --- a/packages/opencode/test/session/session.test.ts +++ b/packages/opencode/test/session/session.test.ts @@ -238,6 +238,38 @@ describe("Session", () => { }), ) + it.instance("forks the chronological prefix across mixed message ID ordering", () => + Effect.gen(function* () { + const session = yield* SessionNs.Service + const created = yield* Effect.acquireRelease(session.create({}), (info) => + session.remove(info.id).pipe(Effect.ignore), + ) + const ids = ["msg_z9-before", "msg_z1-before-wrap", "msg_a0-after-wrap", "msg_a1-after"] + for (const [index, id] of ids.entries()) { + yield* session.updateMessage({ + id: MessageID.make(id), + sessionID: created.id, + role: "user", + time: { created: index + 1 }, + agent: "user", + model: { providerID: "test", modelID: "test" }, + } as SessionV1.User) + } + + const beforeWrap = yield* Effect.acquireRelease( + session.fork({ sessionID: created.id, messageID: MessageID.make(ids[1]!) }), + (info) => session.remove(info.id).pipe(Effect.ignore), + ) + const afterWrap = yield* Effect.acquireRelease( + session.fork({ sessionID: created.id, messageID: MessageID.make(ids[2]!) }), + (info) => session.remove(info.id).pipe(Effect.ignore), + ) + + expect((yield* session.messages({ sessionID: beforeWrap.id })).map((msg) => msg.info.time.created)).toEqual([1]) + expect((yield* session.messages({ sessionID: afterWrap.id })).map((msg) => msg.info.time.created)).toEqual([1, 2]) + }), + ) + it.instance("omits metadata when not provided", () => Effect.gen(function* () { const session = yield* SessionNs.Service