From 40ecfceec2ee218d7b841290afa3517b991efae5 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 26 Jun 2026 05:04:14 +0200 Subject: [PATCH] fix(sdk): wake embedded session execution (#33992) --- .../core/src/control-plane/move-session.ts | 10 +++++----- packages/core/src/session.ts | 1 - packages/core/test/move-session.test.ts | 3 +-- packages/opencode/src/session/session.ts | 11 +++++++--- .../test/server/httpapi-session.test.ts | 20 +++++++++++++++++-- .../opencode/test/session/compaction.test.ts | 3 +-- packages/opencode/test/session/prompt.test.ts | 3 +-- packages/sdk-next/test/embedded.test.ts | 13 ++++++++++++ packages/server/src/handlers.ts | 8 ++++++-- 9 files changed, 53 insertions(+), 19 deletions(-) diff --git a/packages/core/src/control-plane/move-session.ts b/packages/core/src/control-plane/move-session.ts index c217562347..21397ac573 100644 --- a/packages/core/src/control-plane/move-session.ts +++ b/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), ) diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 59c66b760a..81b96ab661 100644 --- a/packages/core/src/session.ts +++ b/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), diff --git a/packages/core/test/move-session.test.ts b/packages/core/test/move-session.test.ts index 0a7db8826f..06b40c0154 100644 --- a/packages/core/test/move-session.test.ts +++ b/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, ), ) diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 1d1ce4c07d..48c43ca0e8 100644 --- a/packages/opencode/src/session/session.ts +++ b/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), ) diff --git a/packages/opencode/test/server/httpapi-session.test.ts b/packages/opencode/test/server/httpapi-session.test.ts index ae72bc4d0a..828ed3373a 100644 --- a/packages/opencode/test/server/httpapi-session.test.ts +++ b/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 } }, ) diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index dc79435c66..92fc6ee622 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/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", diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index ef7b1294cb..460f1acc74 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/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 diff --git a/packages/sdk-next/test/embedded.test.ts b/packages/sdk-next/test/embedded.test.ts index df1b72dbfc..5ba505cbca 100644 --- a/packages/sdk-next/test/embedded.test.ts +++ b/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) diff --git a/packages/server/src/handlers.ts b/packages/server/src/handlers.ts index f12e9b625f..33ecd4117d 100644 --- a/packages/server/src/handlers.ts +++ b/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),