瀏覽代碼

fix(sdk): wake embedded session execution (#33992)

Kit Langton 1 月之前
父節點
當前提交
40ecfceec2

+ 5 - 5
packages/core/src/control-plane/move-session.ts

@@ -6,9 +6,9 @@ import { Git } from "../git"
 import { Location } from "../location"
 import { ProjectV2 } from "../project"
 import { SessionV2 } from "../session"
-import { SessionExecution } from "../session/execution"
 import { SessionEvent } from "../session/event"
 import { SessionSchema } from "../session/schema"
+import { SessionStore } from "../session/store"
 import { AbsolutePath, RelativePath } from "../schema"
 import path from "path"
 
@@ -71,10 +71,11 @@ export const layer = Layer.effect(
     const git = yield* Git.Service
     const events = yield* EventV2.Service
     const project = yield* ProjectV2.Service
-    const session = yield* SessionV2.Service
+    const sessions = yield* SessionStore.Service
 
     const moveSession = Effect.fn("MoveSession.moveSession")(function* (input: Input) {
-      const current = yield* session.get(input.sessionID)
+      const current = yield* sessions.get(input.sessionID)
+      if (!current) return yield* new SessionV2.NotFoundError({ sessionID: input.sessionID })
       const directory = AbsolutePath.make(input.destination.directory)
       if (current.location.directory === directory) return
 
@@ -143,6 +144,5 @@ export const defaultLayer = layer.pipe(
   Layer.provide(Git.defaultLayer),
   Layer.provide(EventV2.defaultLayer),
   Layer.provide(ProjectV2.defaultLayer),
-  Layer.provide(SessionExecution.noopLayer),
-  Layer.provide(SessionV2.defaultLayer),
+  Layer.provide(SessionStore.defaultLayer),
 )

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

@@ -443,7 +443,6 @@ export const defaultLayer = layer.pipe(
   Layer.provide(
     Layer.unwrap(Effect.promise(() => import("./location-layer")).pipe(Effect.map((m) => m.LocationServiceMap.layer))),
   ),
-  Layer.provide(SessionExecution.noopLayer),
   Layer.provide(SessionStore.defaultLayer),
   Layer.provide(SessionProjector.defaultLayer),
   Layer.provide(EventV2.defaultLayer),

+ 1 - 2
packages/core/test/move-session.test.ts

@@ -42,7 +42,7 @@ const layer = MoveSession.layer.pipe(
   Layer.provide(Git.defaultLayer),
   Layer.provide(EventV2.defaultLayer),
   Layer.provide(project),
-  Layer.provide(sessions),
+  Layer.provide(SessionStore.defaultLayer),
 )
 const it = testEffect(
   Layer.mergeAll(
@@ -53,7 +53,6 @@ const it = testEffect(
     project,
     SessionProjector.defaultLayer,
     SessionStore.defaultLayer,
-    SessionExecution.noopLayer,
     sessions,
   ),
 )

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

@@ -12,7 +12,8 @@ import { Database } from "@opencode-ai/core/database/database"
 import { makeRuntime } from "@opencode-ai/core/effect/runtime"
 import { EventV2Bridge } from "@/event-v2-bridge"
 import { SessionV2 } from "@opencode-ai/core/session"
-import { SessionExecution } from "@opencode-ai/core/session/execution"
+import * as SessionExecutionLocal from "@opencode-ai/core/session/execution/local"
+import { LocationServiceMap } from "@opencode-ai/core/location-layer"
 
 import { NotFoundError } from "@/storage/storage"
 import { eq } from "drizzle-orm"
@@ -943,8 +944,12 @@ export const defaultLayer = layer.pipe(
   Layer.provide(BackgroundJob.defaultLayer),
   Layer.provide(Database.defaultLayer),
   Layer.provide(EventV2Bridge.defaultLayer),
-  Layer.provide(SessionExecution.noopLayer),
-  Layer.provide(SessionV2.defaultLayer),
+  Layer.provide(
+    SessionV2.defaultLayer.pipe(
+      Layer.provide(SessionExecutionLocal.defaultLayer),
+      Layer.provide(LocationServiceMap.layer),
+    ),
+  ),
   Layer.provide(RuntimeFlags.defaultLayer),
 )
 

+ 18 - 2
packages/opencode/test/server/httpapi-session.test.ts

@@ -34,7 +34,7 @@ import { resetDatabase } from "../fixture/db"
 import { disposeAllInstances, provideInstanceEffect, TestInstance, tmpdirScoped } from "../fixture/fixture"
 import { TestLLMServer } from "../lib/llm-server"
 import { testProviderConfig } from "../lib/test-provider"
-import { testEffect } from "../lib/effect"
+import { pollWithTimeout, testEffect } from "../lib/effect"
 
 const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
 const workspaceLayer = Workspace.defaultLayer.pipe(
@@ -581,7 +581,7 @@ describe("session HttpApi", () => {
           request(`/api/session/${session.id}/prompt`, {
             method: "POST",
             headers: { ...headers, "content-type": "application/json" },
-            body: JSON.stringify({ id: "msg_http_prompt", prompt: { text: "hello" } }),
+            body: JSON.stringify({ id: "msg_http_prompt", prompt: { text: "hello" }, resume: false }),
           })
         const first = yield* recordPrompt()
         const retried = yield* recordPrompt()
@@ -624,6 +624,22 @@ describe("session HttpApi", () => {
           message: "Prompt message ID conflicts with an existing durable record: msg_http_prompt",
           resource: "msg_http_prompt",
         })
+
+        const wakeID = SessionMessage.ID.make("msg_http_wake")
+        const wake = yield* request(`/api/session/${session.id}/prompt`, {
+          method: "POST",
+          headers: { ...headers, "content-type": "application/json" },
+          body: JSON.stringify({ id: wakeID, prompt: { text: "hello again" } }),
+        })
+        expect(wake.status).toBe(200)
+        const message = yield* pollWithTimeout(
+          requestJson<{ data: SessionMessage.Message[] }>(`/api/session/${session.id}/message`, { headers }).pipe(
+            Effect.map(({ data }) => data.find((message) => message.id === wakeID)),
+          ),
+          "V2 prompt was not promoted after wake",
+          "10 seconds",
+        )
+        expect(message).toMatchObject({ id: wakeID, type: "user" })
       }),
     { git: true, config: { formatter: false, lsp: false } },
   )

+ 1 - 2
packages/opencode/test/session/compaction.test.ts

@@ -613,8 +613,7 @@ describe("session.compaction.create", () => {
         })
 
         const v2 = yield* SessionV2.Service.use((svc) => svc.messages({ sessionID: info.id })).pipe(
-          Effect.provide(SessionExecution.noopLayer),
-          Effect.provide(SessionV2.defaultLayer),
+          Effect.provide(SessionV2.defaultLayer.pipe(Layer.provide(SessionExecution.noopLayer))),
         )
         expect(v2.at(-1)).toMatchObject({
           type: "compaction",

+ 1 - 2
packages/opencode/test/session/prompt.test.ts

@@ -697,8 +697,7 @@ noLLMServer.instance.skip(
       })
 
       const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
-        Effect.provide(SessionExecution.noopLayer),
-        Effect.provide(SessionV2.defaultLayer),
+        Effect.provide(SessionV2.defaultLayer.pipe(Layer.provide(SessionExecution.noopLayer))),
       )
       const { db } = yield* Database.Service
       const row = yield* db

+ 13 - 0
packages/sdk-next/test/embedded.test.ts

@@ -39,6 +39,17 @@ test("embedded client uses the real router and handlers", async () => {
         resume: false,
       })
       const context = yield* opencode.sessions.context({ sessionID })
+      const wake = yield* opencode.sessions.prompt({
+        sessionID,
+        prompt: Prompt.make({ text: "Promote this input" }),
+      })
+      const prompted = yield* opencode.sessions.events({ sessionID }).pipe(
+        Stream.filter((event) => event.type === "session.next.prompted" && event.data.messageID === wake.id),
+        Stream.runHead,
+        Effect.timeout("10 seconds"),
+        Effect.map(Option.getOrThrow),
+      )
+      const wakeContext = yield* opencode.sessions.context({ sessionID })
       const event = yield* opencode.sessions
         .events({ sessionID })
         .pipe(Stream.take(1), Stream.runHead, Effect.map(Option.getOrUndefined))
@@ -71,6 +82,8 @@ test("embedded client uses the real router and handlers", async () => {
       expect(selected.model?.providerID).toBe(model.providerID)
       expect(page.data.some((session) => session.id === sessionID)).toBe(true)
       expect(admitted.sessionID).toBe(sessionID)
+      expect(prompted.type).toBe("session.next.prompted")
+      expect(wakeContext).toContainEqual(expect.objectContaining({ id: wake.id, type: "user" }))
       expect(context.some((message) => message.type === "model-switched")).toBe(true)
       expect(event).toMatchObject({ type: "session.next.model.switched", durable: { seq: 1 } })
       expect(message).toEqual(modelMessage)

+ 6 - 2
packages/server/src/handlers.ts

@@ -26,6 +26,11 @@ import { CredentialHandler } from "./handlers/credential"
 import { Credential } from "@opencode-ai/core/credential"
 import { ProjectCopyHandler } from "./handlers/project-copy"
 
+const sessionLayer = SessionV2.defaultLayer.pipe(
+  Layer.provide(SessionExecutionLocal.defaultLayer),
+  Layer.provide(LocationServiceMap.layer),
+)
+
 export const handlers = Layer.mergeAll(
   HealthHandler,
   LocationHandler,
@@ -48,8 +53,7 @@ export const handlers = Layer.mergeAll(
 ).pipe(
   Layer.provide(sessionLocationLayer),
   Layer.provide(locationLayer),
-  Layer.provide(SessionV2.defaultLayer),
-  Layer.provide(SessionExecutionLocal.defaultLayer),
+  Layer.provide(sessionLayer),
   Layer.provide(PermissionSaved.defaultLayer),
   Layer.provide(PtyTicket.defaultLayer),
   Layer.provide(LocationServiceMap.layer),