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

This commit is contained in:
Kit Langton 2026-06-26 05:04:14 +02:00 committed by GitHub
parent a1f093a748
commit 40ecfceec2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 53 additions and 19 deletions

View file

@ -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),
)

View file

@ -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),

View file

@ -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,
),
)

View file

@ -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),
)

View file

@ -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 } },
)

View file

@ -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",

View file

@ -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

View file

@ -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)

View file

@ -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),