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

fix(opencode): remove automatic full session diffs (#30127)

Dax 3 месяцев назад
Родитель
Сommit
2f2fcc1654

+ 2 - 1
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

@@ -210,7 +210,8 @@ export function Session() {
   const disabled = createMemo(() => permissions().length > 0 || questions().length > 0)
 
   const pending = createMemo(() => {
-    return messages().findLast((x) => x.role === "assistant" && !x.time.completed)?.id
+    const completed = messages().findLast((x) => x.role === "assistant" && x.time.completed)?.id
+    return messages().findLast((x) => x.role === "assistant" && !x.time.completed && (!completed || x.id > completed))?.id
   })
 
   const lastAssistant = createMemo(() => {

+ 3 - 7
packages/opencode/src/session/session.ts

@@ -25,7 +25,6 @@ import { or } from "drizzle-orm"
 import type { SQL } from "drizzle-orm"
 import { PartTable, SessionTable } from "@opencode-ai/core/session/sql"
 import { ProjectTable } from "@opencode-ai/core/project/sql"
-import { Storage } from "@/storage/storage"
 import * as Log from "@opencode-ai/core/util/log"
 import { MessageV2 } from "./message-v2"
 import type { InstanceContext } from "../project/instance-context"
@@ -536,7 +535,7 @@ export type Patch = Omit<Partial<Info>, "time" | "share" | "summary" | "revert"
 export const layer: Layer.Layer<
   Service,
   never,
-  BackgroundJob.Service | Storage.Service | RuntimeFlags.Service | Database.Service | EventV2Bridge.Service
+  BackgroundJob.Service | RuntimeFlags.Service | Database.Service | EventV2Bridge.Service
 > = Layer.effect(
   Service,
   Effect.gen(function* () {
@@ -544,7 +543,6 @@ export const layer: Layer.Layer<
     const database = yield* Database.Service
     const background = yield* BackgroundJob.Service
     const events = yield* EventV2Bridge.Service
-    const storage = yield* Storage.Service
     const flags = yield* RuntimeFlags.Service
 
     const locationForSession = Effect.fnUntraced(function* (sessionID: SessionID) {
@@ -887,9 +885,8 @@ export const layer: Layer.Layer<
     })
 
     const diff = Effect.fn("Session.diff")(function* (sessionID: SessionID) {
-      return yield* storage
-        .read<Snapshot.FileDiff[]>(["session_diff", sessionID])
-        .pipe(Effect.orElseSucceed((): Snapshot.FileDiff[] => []))
+      void sessionID
+      return [] as Snapshot.FileDiff[]
     })
 
     const messages: Interface["messages"] = Effect.fn("Session.messages")(function* (input) {
@@ -1013,7 +1010,6 @@ export const layer: Layer.Layer<
 
 export const defaultLayer = layer.pipe(
   Layer.provide(BackgroundJob.defaultLayer),
-  Layer.provide(Storage.defaultLayer),
   Layer.provide(Database.defaultLayer),
   Layer.provide(EventV2Bridge.defaultLayer),
   Layer.provide(SessionV2.defaultLayer),

+ 17 - 20
packages/opencode/src/session/summary.ts

@@ -2,10 +2,9 @@ import { Effect, Layer, Context, Schema } from "effect"
 import { SessionLegacy } from "@opencode-ai/core/session/legacy"
 import { EventV2Bridge } from "@/event-v2-bridge"
 import { Snapshot } from "@/snapshot"
-import { Storage } from "@/storage/storage"
 import * as Session from "./session"
-import { MessageV2 } from "./message-v2"
 import { SessionID, MessageID } from "./schema"
+import { Config } from "@/config/config"
 
 function unquoteGitPath(input: string) {
   if (!input.startsWith('"')) return input
@@ -76,8 +75,8 @@ export const layer = Layer.effect(
   Effect.gen(function* () {
     const sessions = yield* Session.Service
     const snapshot = yield* Snapshot.Service
-    const storage = yield* Storage.Service
     const events = yield* EventV2Bridge.Service
+    const config = yield* Config.Service
 
     const computeDiff = Effect.fn("SessionSummary.computeDiff")(function* (input: {
       messages: SessionLegacy.WithParts[]
@@ -105,20 +104,18 @@ export const layer = Layer.effect(
       sessionID: SessionID
       messageID: MessageID
     }) {
-      const all = yield* sessions.messages({ sessionID: input.sessionID }).pipe(Effect.orDie)
-      if (!all.length) return
-
-      const diffs = yield* computeDiff({ messages: all })
       yield* sessions.setSummary({
         sessionID: input.sessionID,
         summary: {
-          additions: diffs.reduce((sum, x) => sum + x.additions, 0),
-          deletions: diffs.reduce((sum, x) => sum + x.deletions, 0),
-          files: diffs.length,
+          additions: 0,
+          deletions: 0,
+          files: 0,
         },
       })
-      yield* storage.write(["session_diff", input.sessionID], diffs).pipe(Effect.ignore)
-      yield* events.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: diffs })
+      yield* events.publish(Session.Event.Diff, { sessionID: input.sessionID, diff: [] })
+      if ((yield* config.get()).snapshot === false) return
+      const all = yield* sessions.messages({ sessionID: input.sessionID }).pipe(Effect.orDie)
+      if (!all.length) return
 
       const messages = all.filter(
         (m) => m.info.id === input.messageID || (m.info.role === "assistant" && m.info.parentID === input.messageID),
@@ -131,18 +128,18 @@ export const layer = Layer.effect(
     })
 
     const diff = Effect.fn("SessionSummary.diff")(function* (input: { sessionID: SessionID; messageID?: MessageID }) {
-      const diffs = yield* storage
-        .read<Snapshot.FileDiff[]>(["session_diff", input.sessionID])
-        .pipe(Effect.catch(() => Effect.succeed([] as Snapshot.FileDiff[])))
-      const next = diffs.map((item) => {
+      if (!input.messageID) return []
+      const message = (yield* sessions.messages({ sessionID: input.sessionID }).pipe(Effect.orDie)).find(
+        (item) => item.info.id === input.messageID,
+      )
+      if (!message || message.info.role !== "user") return []
+      const diffs = message.info.summary?.diffs ?? []
+      return diffs.map((item) => {
         if (item.file === undefined) return item
         const file = unquoteGitPath(item.file)
         if (file === item.file) return item
         return { ...item, file }
       })
-      const changed = next.some((item, i) => item.file !== diffs[i]?.file)
-      if (changed) yield* storage.write(["session_diff", input.sessionID], next).pipe(Effect.ignore)
-      return next
     })
 
     return Service.of({ summarize, diff, computeDiff })
@@ -153,8 +150,8 @@ export const defaultLayer = Layer.suspend(() =>
   layer.pipe(
     Layer.provide(Session.defaultLayer),
     Layer.provide(Snapshot.defaultLayer),
-    Layer.provide(Storage.defaultLayer),
     Layer.provide(EventV2Bridge.defaultLayer),
+    Layer.provide(Config.defaultLayer),
   ),
 )
 

+ 38 - 12
packages/opencode/test/server/session-diff-missing-patch.test.ts

@@ -4,16 +4,20 @@
  * the response was Schema-encoded against `Snapshot.FileDiff` with
  * `patch: Schema.String` (required), so any session whose stored
  * `summary_diffs` had a row without `patch` returned HTTP 400 and the
- * session never loaded.
+ * session never loaded. Legacy session-level diffs are no longer surfaced,
+ * but the endpoint remains compatible and must still return successfully.
  *
  * This test inserts a session row with a missing-patch diff entry and
- * asserts that GET /session/<id>/diff returns 200 with the row intact.
+ * asserts that GET /session/<id>/diff returns 200 with empty data.
  */
 import { afterEach, describe, expect } from "bun:test"
 import { Effect, Layer } from "effect"
 import { SessionPaths } from "@/server/routes/instance/httpapi/groups/session"
 import { Session } from "@/session/session"
 import { Storage } from "@/storage/storage"
+import { SessionLegacy } from "@opencode-ai/core/session/legacy"
+import { MessageID } from "@/session/schema"
+import { ProviderV2 } from "@opencode-ai/core/provider"
 import { resetDatabase } from "../fixture/db"
 import { disposeAllInstances, TestInstance } from "../fixture/fixture"
 import { testEffect } from "../lib/effect"
@@ -38,7 +42,7 @@ const withSession = (input?: Parameters<Session.Interface["create"]>[0]) =>
 
 describe("session diff with missing patch (#26574)", () => {
   it.instance(
-    "GET /session/<id>/diff returns 200 when summary_diffs row has no patch",
+    "GET /session/<id>/diff ignores legacy session-level diff storage",
     () =>
       Effect.gen(function* () {
         const test = yield* TestInstance
@@ -57,15 +61,37 @@ describe("session diff with missing patch (#26574)", () => {
         )
 
         expect(response.status).toBe(200)
-        const body = (yield* response.json) as Array<{
-          file: string
-          patch?: string
-          additions: number
-        }>
-        expect(body).toHaveLength(1)
-        expect(body[0]?.file).toBe("legacy.txt")
-        expect(body[0]?.additions).toBe(1)
-        expect(body[0]?.patch).toBeUndefined()
+        expect(yield* response.json).toEqual([])
+      }),
+    { git: true, config: { formatter: false, lsp: false } },
+  )
+
+  it.instance(
+    "GET /session/<id>/diff returns requested turn diffs",
+    () =>
+      Effect.gen(function* () {
+        const test = yield* TestInstance
+        const session = yield* withSession({ title: "turn-diff" })
+        const messageID = MessageID.ascending()
+        yield* Session.use.updateMessage({
+          id: messageID,
+          sessionID: session.id,
+          role: "user",
+          time: { created: Date.now() },
+          agent: "build",
+          model: { providerID: ProviderV2.ID.make("test"), modelID: ProviderV2.ModelID.make("model") },
+          summary: {
+            diffs: [{ file: "turn.ts", additions: 1, deletions: 0, status: "modified" }],
+          },
+        } satisfies SessionLegacy.User)
+
+        const response = yield* requestInDirectory(
+          `${pathFor(SessionPaths.diff, { sessionID: session.id })}?messageID=${messageID}`,
+          test.directory,
+        )
+
+        expect(response.status).toBe(200)
+        expect(yield* response.json).toEqual([{ file: "turn.ts", additions: 1, deletions: 0, status: "modified" }])
       }),
     { git: true, config: { formatter: false, lsp: false } },
   )

+ 4 - 2
packages/opencode/test/session/snapshot-tool-race.test.ts

@@ -257,15 +257,17 @@ it.live("tool execution produces non-empty session diff (snapshot race)", () =>
 
       // Verify the tool call completed (in the first assistant message)
       const allMsgs = yield* MessageV2.filterCompactedEffect(session.id)
+      const user = allMsgs.find((msg): msg is SessionLegacy.WithParts & { info: SessionLegacy.User } => msg.info.role === "user")
       const tool = allMsgs
         .flatMap((m) => m.parts)
         .find((p): p is SessionLegacy.ToolPart => p.type === "tool" && p.tool === "bash")
       expect(tool?.state.status).toBe("completed")
+      if (!user) throw new Error("Expected user message")
 
-      // Poll for diff — summarize() is fire-and-forget
+      // Poll for the turn diff — summarize() is fire-and-forget.
       let diff: Array<{ file?: string }> = []
       for (let i = 0; i < 50; i++) {
-        diff = yield* summary.diff({ sessionID: session.id })
+        diff = yield* summary.diff({ sessionID: session.id, messageID: user.info.id })
         if (diff.length > 0) break
         yield* Effect.sleep("100 millis")
       }