Просмотр исходного кода

fix(core): commit staged revert before admitting new prompt (#34338)

Dax 1 месяц назад
Родитель
Сommit
42a3cf9645
2 измененных файлов с 56 добавлено и 2 удалено
  1. 5 1
      packages/core/src/session.ts
  2. 51 1
      packages/core/test/session-prompt.test.ts

+ 5 - 1
packages/core/src/session.ts

@@ -370,7 +370,11 @@ export const layer = Layer.effect(
       prompt: Effect.fn("V2Session.prompt")((input) =>
         Effect.uninterruptible(
           Effect.gen(function* () {
-            yield* result.get(input.sessionID)
+            const session = yield* result.get(input.sessionID)
+            // A staged revert must be committed before admitting new input so the prompt
+            // continues from the reverted boundary rather than stale post-boundary history.
+            if (session.revert)
+              yield* SessionRevert.commit(session).pipe(Effect.provideService(EventV2.Service, events))
             const prompt = resolvePrompt(input.prompt)
             const messageID = input.id ?? SessionMessage.ID.create()
             const delivery = input.delivery ?? "steer"

+ 51 - 1
packages/core/test/session-prompt.test.ts

@@ -1,10 +1,12 @@
 import { describe, expect } from "bun:test"
-import { DateTime, Effect, Fiber, Layer, Stream } from "effect"
+import { DateTime, Effect, Fiber, Layer, Schema, Stream } from "effect"
 import { eq } from "drizzle-orm"
 import { Database } from "@opencode-ai/core/database/database"
 import { EventV2 } from "@opencode-ai/core/event"
 import { EventTable } from "@opencode-ai/core/event/sql"
 import { SessionEvent } from "@opencode-ai/core/session/event"
+import { ModelV2 } from "@opencode-ai/core/model"
+import { ProviderV2 } from "@opencode-ai/core/provider"
 import { Project } from "@opencode-ai/core/project"
 import { ProjectTable } from "@opencode-ai/core/project/sql"
 import { AbsolutePath } from "@opencode-ai/core/schema"
@@ -110,6 +112,21 @@ const eventCount = (type: string) =>
       ),
   )
 
+const encodeMessage = Schema.encodeSync(SessionMessage.Message)
+const assistantRow = (id: SessionMessage.ID, seq: number) => {
+  const { id: _, type, ...data } = encodeMessage(
+    SessionMessage.Assistant.make({
+      id,
+      type: "assistant",
+      agent: "build",
+      model: { id: ModelV2.ID.make("model"), providerID: ProviderV2.ID.make("provider") },
+      content: [],
+      time: { created: DateTime.makeUnsafe(0) },
+    }),
+  )
+  return { id, session_id: sessionID, type, seq, time_created: 0, data }
+}
+
 describe("SessionV2.prompt", () => {
   it.effect("exposes the execution registry", () =>
     Effect.gen(function* () {
@@ -201,6 +218,39 @@ describe("SessionV2.prompt", () => {
     }),
   )
 
+  it.effect("commits a staged revert before admitting a new prompt", () =>
+    Effect.gen(function* () {
+      yield* setup
+      const session = yield* SessionV2.Service
+      const events = yield* EventV2.Service
+      const { db } = yield* Database.Service
+
+      const boundary = yield* session.prompt({
+        sessionID,
+        prompt: Prompt.make({ text: "boundary" }),
+        resume: false,
+      })
+      yield* SessionInput.promoteSteers(db, events, sessionID, Number.MAX_SAFE_INTEGER)
+      const stale = SessionMessage.ID.make("msg_stale_assistant")
+      yield* db.insert(SessionMessageTable).values(assistantRow(stale, 100)).run().pipe(Effect.orDie)
+      yield* events.publish(SessionEvent.RevertEvent.Staged, {
+        sessionID,
+        timestamp: yield* DateTime.now,
+        revert: { messageID: boundary.id, files: [] },
+      })
+      expect((yield* session.get(sessionID)).revert?.messageID).toBe(boundary.id)
+
+      yield* session.prompt({ sessionID, prompt: Prompt.make({ text: "after revert" }), resume: false })
+
+      expect((yield* session.get(sessionID)).revert).toBeUndefined()
+      expect(
+        (yield* db.select({ id: SessionMessageTable.id }).from(SessionMessageTable).all().pipe(Effect.orDie)).map(
+          (row) => row.id,
+        ),
+      ).not.toContain(stale)
+    }),
+  )
+
   it.effect("resolves attachment MIME before admission", () =>
     Effect.gen(function* () {
       yield* setup