mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 10:16:03 +00:00
fix(sdk): wake embedded session execution (#33992)
This commit is contained in:
@@ -6,9 +6,9 @@ import { Git } from "../git"
|
|||||||
import { Location } from "../location"
|
import { Location } from "../location"
|
||||||
import { ProjectV2 } from "../project"
|
import { ProjectV2 } from "../project"
|
||||||
import { SessionV2 } from "../session"
|
import { SessionV2 } from "../session"
|
||||||
import { SessionExecution } from "../session/execution"
|
|
||||||
import { SessionEvent } from "../session/event"
|
import { SessionEvent } from "../session/event"
|
||||||
import { SessionSchema } from "../session/schema"
|
import { SessionSchema } from "../session/schema"
|
||||||
|
import { SessionStore } from "../session/store"
|
||||||
import { AbsolutePath, RelativePath } from "../schema"
|
import { AbsolutePath, RelativePath } from "../schema"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
@@ -71,10 +71,11 @@ export const layer = Layer.effect(
|
|||||||
const git = yield* Git.Service
|
const git = yield* Git.Service
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const project = yield* ProjectV2.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 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)
|
const directory = AbsolutePath.make(input.destination.directory)
|
||||||
if (current.location.directory === directory) return
|
if (current.location.directory === directory) return
|
||||||
|
|
||||||
@@ -143,6 +144,5 @@ export const defaultLayer = layer.pipe(
|
|||||||
Layer.provide(Git.defaultLayer),
|
Layer.provide(Git.defaultLayer),
|
||||||
Layer.provide(EventV2.defaultLayer),
|
Layer.provide(EventV2.defaultLayer),
|
||||||
Layer.provide(ProjectV2.defaultLayer),
|
Layer.provide(ProjectV2.defaultLayer),
|
||||||
Layer.provide(SessionExecution.noopLayer),
|
Layer.provide(SessionStore.defaultLayer),
|
||||||
Layer.provide(SessionV2.defaultLayer),
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -443,7 +443,6 @@ export const defaultLayer = layer.pipe(
|
|||||||
Layer.provide(
|
Layer.provide(
|
||||||
Layer.unwrap(Effect.promise(() => import("./location-layer")).pipe(Effect.map((m) => m.LocationServiceMap.layer))),
|
Layer.unwrap(Effect.promise(() => import("./location-layer")).pipe(Effect.map((m) => m.LocationServiceMap.layer))),
|
||||||
),
|
),
|
||||||
Layer.provide(SessionExecution.noopLayer),
|
|
||||||
Layer.provide(SessionStore.defaultLayer),
|
Layer.provide(SessionStore.defaultLayer),
|
||||||
Layer.provide(SessionProjector.defaultLayer),
|
Layer.provide(SessionProjector.defaultLayer),
|
||||||
Layer.provide(EventV2.defaultLayer),
|
Layer.provide(EventV2.defaultLayer),
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const layer = MoveSession.layer.pipe(
|
|||||||
Layer.provide(Git.defaultLayer),
|
Layer.provide(Git.defaultLayer),
|
||||||
Layer.provide(EventV2.defaultLayer),
|
Layer.provide(EventV2.defaultLayer),
|
||||||
Layer.provide(project),
|
Layer.provide(project),
|
||||||
Layer.provide(sessions),
|
Layer.provide(SessionStore.defaultLayer),
|
||||||
)
|
)
|
||||||
const it = testEffect(
|
const it = testEffect(
|
||||||
Layer.mergeAll(
|
Layer.mergeAll(
|
||||||
@@ -53,7 +53,6 @@ const it = testEffect(
|
|||||||
project,
|
project,
|
||||||
SessionProjector.defaultLayer,
|
SessionProjector.defaultLayer,
|
||||||
SessionStore.defaultLayer,
|
SessionStore.defaultLayer,
|
||||||
SessionExecution.noopLayer,
|
|
||||||
sessions,
|
sessions,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import { Database } from "@opencode-ai/core/database/database"
|
|||||||
import { makeRuntime } from "@opencode-ai/core/effect/runtime"
|
import { makeRuntime } from "@opencode-ai/core/effect/runtime"
|
||||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||||
import { SessionV2 } from "@opencode-ai/core/session"
|
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 { NotFoundError } from "@/storage/storage"
|
||||||
import { eq } from "drizzle-orm"
|
import { eq } from "drizzle-orm"
|
||||||
@@ -943,8 +944,12 @@ export const defaultLayer = layer.pipe(
|
|||||||
Layer.provide(BackgroundJob.defaultLayer),
|
Layer.provide(BackgroundJob.defaultLayer),
|
||||||
Layer.provide(Database.defaultLayer),
|
Layer.provide(Database.defaultLayer),
|
||||||
Layer.provide(EventV2Bridge.defaultLayer),
|
Layer.provide(EventV2Bridge.defaultLayer),
|
||||||
Layer.provide(SessionExecution.noopLayer),
|
Layer.provide(
|
||||||
Layer.provide(SessionV2.defaultLayer),
|
SessionV2.defaultLayer.pipe(
|
||||||
|
Layer.provide(SessionExecutionLocal.defaultLayer),
|
||||||
|
Layer.provide(LocationServiceMap.layer),
|
||||||
|
),
|
||||||
|
),
|
||||||
Layer.provide(RuntimeFlags.defaultLayer),
|
Layer.provide(RuntimeFlags.defaultLayer),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { resetDatabase } from "../fixture/db"
|
|||||||
import { disposeAllInstances, provideInstanceEffect, TestInstance, tmpdirScoped } from "../fixture/fixture"
|
import { disposeAllInstances, provideInstanceEffect, TestInstance, tmpdirScoped } from "../fixture/fixture"
|
||||||
import { TestLLMServer } from "../lib/llm-server"
|
import { TestLLMServer } from "../lib/llm-server"
|
||||||
import { testProviderConfig } from "../lib/test-provider"
|
import { testProviderConfig } from "../lib/test-provider"
|
||||||
import { testEffect } from "../lib/effect"
|
import { pollWithTimeout, testEffect } from "../lib/effect"
|
||||||
|
|
||||||
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
const originalWorkspaces = Flag.OPENCODE_EXPERIMENTAL_WORKSPACES
|
||||||
const workspaceLayer = Workspace.defaultLayer.pipe(
|
const workspaceLayer = Workspace.defaultLayer.pipe(
|
||||||
@@ -581,7 +581,7 @@ describe("session HttpApi", () => {
|
|||||||
request(`/api/session/${session.id}/prompt`, {
|
request(`/api/session/${session.id}/prompt`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { ...headers, "content-type": "application/json" },
|
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 first = yield* recordPrompt()
|
||||||
const retried = 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",
|
message: "Prompt message ID conflicts with an existing durable record: msg_http_prompt",
|
||||||
resource: "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 } },
|
{ git: true, config: { formatter: false, lsp: false } },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -613,8 +613,7 @@ describe("session.compaction.create", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const v2 = yield* SessionV2.Service.use((svc) => svc.messages({ sessionID: info.id })).pipe(
|
const v2 = yield* SessionV2.Service.use((svc) => svc.messages({ sessionID: info.id })).pipe(
|
||||||
Effect.provide(SessionExecution.noopLayer),
|
Effect.provide(SessionV2.defaultLayer.pipe(Layer.provide(SessionExecution.noopLayer))),
|
||||||
Effect.provide(SessionV2.defaultLayer),
|
|
||||||
)
|
)
|
||||||
expect(v2.at(-1)).toMatchObject({
|
expect(v2.at(-1)).toMatchObject({
|
||||||
type: "compaction",
|
type: "compaction",
|
||||||
|
|||||||
@@ -697,8 +697,7 @@ noLLMServer.instance.skip(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
|
const messages = yield* SessionV2.Service.use((session) => session.messages({ sessionID: chat.id })).pipe(
|
||||||
Effect.provide(SessionExecution.noopLayer),
|
Effect.provide(SessionV2.defaultLayer.pipe(Layer.provide(SessionExecution.noopLayer))),
|
||||||
Effect.provide(SessionV2.defaultLayer),
|
|
||||||
)
|
)
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
const row = yield* db
|
const row = yield* db
|
||||||
|
|||||||
@@ -39,6 +39,17 @@ test("embedded client uses the real router and handlers", async () => {
|
|||||||
resume: false,
|
resume: false,
|
||||||
})
|
})
|
||||||
const context = yield* opencode.sessions.context({ sessionID })
|
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
|
const event = yield* opencode.sessions
|
||||||
.events({ sessionID })
|
.events({ sessionID })
|
||||||
.pipe(Stream.take(1), Stream.runHead, Effect.map(Option.getOrUndefined))
|
.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(selected.model?.providerID).toBe(model.providerID)
|
||||||
expect(page.data.some((session) => session.id === sessionID)).toBe(true)
|
expect(page.data.some((session) => session.id === sessionID)).toBe(true)
|
||||||
expect(admitted.sessionID).toBe(sessionID)
|
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(context.some((message) => message.type === "model-switched")).toBe(true)
|
||||||
expect(event).toMatchObject({ type: "session.next.model.switched", durable: { seq: 1 } })
|
expect(event).toMatchObject({ type: "session.next.model.switched", durable: { seq: 1 } })
|
||||||
expect(message).toEqual(modelMessage)
|
expect(message).toEqual(modelMessage)
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ import { CredentialHandler } from "./handlers/credential"
|
|||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
import { ProjectCopyHandler } from "./handlers/project-copy"
|
import { ProjectCopyHandler } from "./handlers/project-copy"
|
||||||
|
|
||||||
|
const sessionLayer = SessionV2.defaultLayer.pipe(
|
||||||
|
Layer.provide(SessionExecutionLocal.defaultLayer),
|
||||||
|
Layer.provide(LocationServiceMap.layer),
|
||||||
|
)
|
||||||
|
|
||||||
export const handlers = Layer.mergeAll(
|
export const handlers = Layer.mergeAll(
|
||||||
HealthHandler,
|
HealthHandler,
|
||||||
LocationHandler,
|
LocationHandler,
|
||||||
@@ -48,8 +53,7 @@ export const handlers = Layer.mergeAll(
|
|||||||
).pipe(
|
).pipe(
|
||||||
Layer.provide(sessionLocationLayer),
|
Layer.provide(sessionLocationLayer),
|
||||||
Layer.provide(locationLayer),
|
Layer.provide(locationLayer),
|
||||||
Layer.provide(SessionV2.defaultLayer),
|
Layer.provide(sessionLayer),
|
||||||
Layer.provide(SessionExecutionLocal.defaultLayer),
|
|
||||||
Layer.provide(PermissionSaved.defaultLayer),
|
Layer.provide(PermissionSaved.defaultLayer),
|
||||||
Layer.provide(PtyTicket.defaultLayer),
|
Layer.provide(PtyTicket.defaultLayer),
|
||||||
Layer.provide(LocationServiceMap.layer),
|
Layer.provide(LocationServiceMap.layer),
|
||||||
|
|||||||
Reference in New Issue
Block a user