Bläddra i källkod

refactor(session): remove async facade exports (#22471)

Kit Langton 5 månader sedan
förälder
incheckning
68384613be

+ 29 - 24
packages/opencode/script/seed-e2e.ts

@@ -33,30 +33,35 @@ const seed = async () => {
           }),
         )
 
-        const session = await Session.create({ title })
-        const messageID = MessageID.ascending()
-        const partID = PartID.ascending()
-        const message = {
-          id: messageID,
-          sessionID: session.id,
-          role: "user" as const,
-          time: { created: now },
-          agent: "build",
-          model: {
-            providerID: ProviderID.make(providerID),
-            modelID: ModelID.make(modelID),
-          },
-        }
-        const part = {
-          id: partID,
-          sessionID: session.id,
-          messageID,
-          type: "text" as const,
-          text,
-          time: { start: now },
-        }
-        await Session.updateMessage(message)
-        await Session.updatePart(part)
+        await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const session = yield* Session.Service
+            const result = yield* session.create({ title })
+            const messageID = MessageID.ascending()
+            const partID = PartID.ascending()
+            const message = {
+              id: messageID,
+              sessionID: result.id,
+              role: "user" as const,
+              time: { created: now },
+              agent: "build",
+              model: {
+                providerID: ProviderID.make(providerID),
+                modelID: ModelID.make(modelID),
+              },
+            }
+            const part = {
+              id: partID,
+              sessionID: result.id,
+              messageID,
+              type: "text" as const,
+              text,
+              time: { start: now },
+            }
+            yield* session.updateMessage(message)
+            yield* session.updatePart(part)
+          }),
+        )
         await AppRuntime.runPromise(
           Project.Service.use((svc) => svc.update({ projectID: Instance.project.id, name: "E2E Project" })),
         )

+ 43 - 39
packages/opencode/src/cli/cmd/debug/agent.ts

@@ -123,45 +123,49 @@ function parseToolParams(input?: string) {
 }
 
 async function createToolContext(agent: Agent.Info) {
-  const session = await Session.create({ title: `Debug tool run (${agent.name})` })
-  const messageID = MessageID.ascending()
-  const model =
-    agent.model ??
-    (await AppRuntime.runPromise(
-      Effect.gen(function* () {
-        const provider = yield* Provider.Service
-        return yield* provider.defaultModel()
-      }),
-    ))
-  const now = Date.now()
-  const message: MessageV2.Assistant = {
-    id: messageID,
-    sessionID: session.id,
-    role: "assistant",
-    time: {
-      created: now,
-    },
-    parentID: messageID,
-    modelID: model.modelID,
-    providerID: model.providerID,
-    mode: "debug",
-    agent: agent.name,
-    path: {
-      cwd: Instance.directory,
-      root: Instance.worktree,
-    },
-    cost: 0,
-    tokens: {
-      input: 0,
-      output: 0,
-      reasoning: 0,
-      cache: {
-        read: 0,
-        write: 0,
-      },
-    },
-  }
-  await Session.updateMessage(message)
+  const { session, messageID } = await AppRuntime.runPromise(
+    Effect.gen(function* () {
+      const session = yield* Session.Service
+      const result = yield* session.create({ title: `Debug tool run (${agent.name})` })
+      const messageID = MessageID.ascending()
+      const model = agent.model
+        ? agent.model
+        : yield* Effect.gen(function* () {
+            const provider = yield* Provider.Service
+            return yield* provider.defaultModel()
+          })
+      const now = Date.now()
+      const message: MessageV2.Assistant = {
+        id: messageID,
+        sessionID: result.id,
+        role: "assistant",
+        time: {
+          created: now,
+        },
+        parentID: messageID,
+        modelID: model.modelID,
+        providerID: model.providerID,
+        mode: "debug",
+        agent: agent.name,
+        path: {
+          cwd: Instance.directory,
+          root: Instance.worktree,
+        },
+        cost: 0,
+        tokens: {
+          input: 0,
+          output: 0,
+          reasoning: 0,
+          cache: {
+            read: 0,
+            write: 0,
+          },
+        },
+      }
+      yield* session.updateMessage(message)
+      return { session: result, messageID }
+    }),
+  )
 
   const ruleset = Permission.merge(agent.permission, session.permission ?? [])
 

+ 12 - 2
packages/opencode/src/cli/cmd/export.ts

@@ -6,6 +6,8 @@ import { bootstrap } from "../bootstrap"
 import { UI } from "../ui"
 import * as prompts from "@clack/prompts"
 import { EOL } from "os"
+import { AppRuntime } from "@/effect/app-runtime"
+import { Effect } from "effect"
 
 export const ExportCommand = cmd({
   command: "export [sessionID]",
@@ -67,8 +69,16 @@ export const ExportCommand = cmd({
       }
 
       try {
-        const sessionInfo = await Session.get(sessionID!)
-        const messages = await Session.messages({ sessionID: sessionInfo.id })
+        const { sessionInfo, messages } = await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const session = yield* Session.Service
+            const sessionInfo = yield* session.get(sessionID!)
+            return {
+              sessionInfo,
+              messages: yield* session.messages({ sessionID: sessionInfo.id }),
+            }
+          }),
+        )
 
         const exportData = {
           info: sessionInfo,

+ 89 - 94
packages/opencode/src/cli/cmd/github.ts

@@ -33,6 +33,7 @@ import { AppRuntime } from "@/effect/app-runtime"
 import { Git } from "@/git"
 import { setTimeout as sleep } from "node:timers/promises"
 import { Process } from "@/util/process"
+import { Effect } from "effect"
 
 type GitHubAuthor = {
   login: string
@@ -551,15 +552,19 @@ export const GithubRunCommand = cmd({
 
         // Setup opencode session
         const repoData = await fetchRepo()
-        session = await Session.create({
-          permission: [
-            {
-              permission: "question",
-              action: "deny",
-              pattern: "*",
-            },
-          ],
-        })
+        session = await AppRuntime.runPromise(
+          Session.Service.use((svc) =>
+            svc.create({
+              permission: [
+                {
+                  permission: "question",
+                  action: "deny",
+                  pattern: "*",
+                },
+              ],
+            }),
+          ),
+        )
         subscribeSessionEvents()
         shareId = await (async () => {
           if (share === false) return
@@ -937,96 +942,86 @@ export const GithubRunCommand = cmd({
       async function chat(message: string, files: PromptFiles = []) {
         console.log("Sending message to opencode...")
 
-        const result = await SessionPrompt.prompt({
-          sessionID: session.id,
-          messageID: MessageID.ascending(),
-          variant,
-          model: {
-            providerID,
-            modelID,
-          },
-          // agent is omitted - server will use default_agent from config or fall back to "build"
-          parts: [
-            {
-              id: PartID.ascending(),
-              type: "text",
-              text: message,
-            },
-            ...files.flatMap((f) => [
-              {
-                id: PartID.ascending(),
-                type: "file" as const,
-                mime: f.mime,
-                url: `data:${f.mime};base64,${f.content}`,
-                filename: f.filename,
-                source: {
-                  type: "file" as const,
-                  text: {
-                    value: f.replacement,
-                    start: f.start,
-                    end: f.end,
-                  },
-                  path: f.filename,
-                },
+        return AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const prompt = yield* SessionPrompt.Service
+            const result = yield* prompt.prompt({
+              sessionID: session.id,
+              messageID: MessageID.ascending(),
+              variant,
+              model: {
+                providerID,
+                modelID,
               },
-            ]),
-          ],
-        })
-
-        // result should always be assistant just satisfying type checker
-        if (result.info.role === "assistant" && result.info.error) {
-          const err = result.info.error
-          console.error("Agent error:", err)
-
-          if (err.name === "ContextOverflowError") {
-            throw new Error(formatPromptTooLargeError(files))
-          }
-
-          const errorMsg = err.data?.message || ""
-          throw new Error(`${err.name}: ${errorMsg}`)
-        }
-
-        const text = extractResponseText(result.parts)
-        if (text) return text
-
-        // No text part (tool-only or reasoning-only) - ask agent to summarize
-        console.log("Requesting summary from agent...")
-        const summary = await SessionPrompt.prompt({
-          sessionID: session.id,
-          messageID: MessageID.ascending(),
-          variant,
-          model: {
-            providerID,
-            modelID,
-          },
-          tools: { "*": false }, // Disable all tools to force text response
-          parts: [
-            {
-              id: PartID.ascending(),
-              type: "text",
-              text: "Summarize the actions (tool calls & reasoning) you did for the user in 1-2 sentences.",
-            },
-          ],
-        })
-
-        if (summary.info.role === "assistant" && summary.info.error) {
-          const err = summary.info.error
-          console.error("Summary agent error:", err)
+              // agent is omitted - server will use default_agent from config or fall back to "build"
+              parts: [
+                {
+                  id: PartID.ascending(),
+                  type: "text",
+                  text: message,
+                },
+                ...files.flatMap((f) => [
+                  {
+                    id: PartID.ascending(),
+                    type: "file" as const,
+                    mime: f.mime,
+                    url: `data:${f.mime};base64,${f.content}`,
+                    filename: f.filename,
+                    source: {
+                      type: "file" as const,
+                      text: {
+                        value: f.replacement,
+                        start: f.start,
+                        end: f.end,
+                      },
+                      path: f.filename,
+                    },
+                  },
+                ]),
+              ],
+            })
 
-          if (err.name === "ContextOverflowError") {
-            throw new Error(formatPromptTooLargeError(files))
-          }
+            if (result.info.role === "assistant" && result.info.error) {
+              const err = result.info.error
+              console.error("Agent error:", err)
+              if (err.name === "ContextOverflowError") throw new Error(formatPromptTooLargeError(files))
+              throw new Error(`${err.name}: ${err.data?.message || ""}`)
+            }
 
-          const errorMsg = err.data?.message || ""
-          throw new Error(`${err.name}: ${errorMsg}`)
-        }
+            const text = extractResponseText(result.parts)
+            if (text) return text
+
+            console.log("Requesting summary from agent...")
+            const summary = yield* prompt.prompt({
+              sessionID: session.id,
+              messageID: MessageID.ascending(),
+              variant,
+              model: {
+                providerID,
+                modelID,
+              },
+              tools: { "*": false },
+              parts: [
+                {
+                  id: PartID.ascending(),
+                  type: "text",
+                  text: "Summarize the actions (tool calls & reasoning) you did for the user in 1-2 sentences.",
+                },
+              ],
+            })
 
-        const summaryText = extractResponseText(summary.parts)
-        if (!summaryText) {
-          throw new Error("Failed to get summary from agent")
-        }
+            if (summary.info.role === "assistant" && summary.info.error) {
+              const err = summary.info.error
+              console.error("Summary agent error:", err)
+              if (err.name === "ContextOverflowError") throw new Error(formatPromptTooLargeError(files))
+              throw new Error(`${err.name}: ${err.data?.message || ""}`)
+            }
 
-        return summaryText
+            const summaryText = extractResponseText(summary.parts)
+            if (!summaryText) throw new Error("Failed to get summary from agent")
+            return summaryText
+          }),
+        )
       }
 
       async function getOidcToken() {

+ 3 - 2
packages/opencode/src/cli/cmd/session.ts

@@ -11,6 +11,7 @@ import { Process } from "../../util/process"
 import { EOL } from "os"
 import path from "path"
 import { which } from "../../util/which"
+import { AppRuntime } from "@/effect/app-runtime"
 
 function pagerCmd(): string[] {
   const lessOptions = ["-R", "-S"]
@@ -60,12 +61,12 @@ export const SessionDeleteCommand = cmd({
     await bootstrap(process.cwd(), async () => {
       const sessionID = SessionID.make(args.sessionID)
       try {
-        await Session.get(sessionID)
+        await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID)))
       } catch {
         UI.error(`Session not found: ${args.sessionID}`)
         process.exit(1)
       }
-      await Session.remove(sessionID)
+      await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(sessionID)))
       UI.println(UI.Style.TEXT_SUCCESS_BOLD + `Session ${args.sessionID} deleted` + UI.Style.TEXT_NORMAL)
     })
   },

+ 4 - 1
packages/opencode/src/cli/cmd/stats.ts

@@ -6,6 +6,7 @@ import { Database } from "../../storage/db"
 import { SessionTable } from "../../session/session.sql"
 import { Project } from "../../project/project"
 import { Instance } from "../../project/instance"
+import { AppRuntime } from "@/effect/app-runtime"
 
 interface SessionStats {
   totalSessions: number
@@ -167,7 +168,9 @@ export async function aggregateSessionStats(days?: number, projectFilter?: strin
     const batch = filteredSessions.slice(i, i + BATCH_SIZE)
 
     const batchPromises = batch.map(async (session) => {
-      const messages = await Session.messages({ sessionID: session.id })
+      const messages = await AppRuntime.runPromise(
+        Session.Service.use((svc) => svc.messages({ sessionID: session.id })),
+      )
 
       let sessionCost = 0
       let sessionTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }

+ 1 - 1
packages/opencode/src/server/instance/middleware.ts

@@ -41,7 +41,7 @@ async function getSessionWorkspace(url: URL) {
   const id = getSessionID(url)
   if (!id) return null
 
-  const session = await Session.get(id).catch(() => undefined)
+  const session = await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(id))).catch(() => undefined)
   return session?.workspaceID
 }
 

+ 65 - 51
packages/opencode/src/server/instance/session.ts

@@ -121,12 +121,12 @@ export const SessionRoutes = lazy(() =>
       validator(
         "param",
         z.object({
-          sessionID: Session.get.schema,
+          sessionID: Session.GetInput,
         }),
       ),
       async (c) => {
         const sessionID = c.req.valid("param").sessionID
-        const session = await Session.get(sessionID)
+        const session = await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID)))
         return c.json(session)
       },
     )
@@ -152,12 +152,12 @@ export const SessionRoutes = lazy(() =>
       validator(
         "param",
         z.object({
-          sessionID: Session.children.schema,
+          sessionID: Session.ChildrenInput,
         }),
       ),
       async (c) => {
         const sessionID = c.req.valid("param").sessionID
-        const session = await Session.children(sessionID)
+        const session = await AppRuntime.runPromise(Session.Service.use((svc) => svc.children(sessionID)))
         return c.json(session)
       },
     )
@@ -209,7 +209,7 @@ export const SessionRoutes = lazy(() =>
           },
         },
       }),
-      validator("json", Session.create.schema),
+      validator("json", Session.CreateInput),
       async (c) => {
         const body = c.req.valid("json") ?? {}
         const session = await AppRuntime.runPromise(SessionShare.Service.use((svc) => svc.create(body)))
@@ -237,12 +237,12 @@ export const SessionRoutes = lazy(() =>
       validator(
         "param",
         z.object({
-          sessionID: Session.remove.schema,
+          sessionID: Session.RemoveInput,
         }),
       ),
       async (c) => {
         const sessionID = c.req.valid("param").sessionID
-        await Session.remove(sessionID)
+        await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(sessionID)))
         return c.json(true)
       },
     )
@@ -285,22 +285,27 @@ export const SessionRoutes = lazy(() =>
       async (c) => {
         const sessionID = c.req.valid("param").sessionID
         const updates = c.req.valid("json")
-        const current = await Session.get(sessionID)
+        const session = await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const session = yield* Session.Service
+            const current = yield* session.get(sessionID)
 
-        if (updates.title !== undefined) {
-          await Session.setTitle({ sessionID, title: updates.title })
-        }
-        if (updates.permission !== undefined) {
-          await Session.setPermission({
-            sessionID,
-            permission: Permission.merge(current.permission ?? [], updates.permission),
-          })
-        }
-        if (updates.time?.archived !== undefined) {
-          await Session.setArchived({ sessionID, time: updates.time.archived })
-        }
+            if (updates.title !== undefined) {
+              yield* session.setTitle({ sessionID, title: updates.title })
+            }
+            if (updates.permission !== undefined) {
+              yield* session.setPermission({
+                sessionID,
+                permission: Permission.merge(current.permission ?? [], updates.permission),
+              })
+            }
+            if (updates.time?.archived !== undefined) {
+              yield* session.setArchived({ sessionID, time: updates.time.archived })
+            }
 
-        const session = await Session.get(sessionID)
+            return yield* session.get(sessionID)
+          }),
+        )
         return c.json(session)
       },
     )
@@ -341,13 +346,17 @@ export const SessionRoutes = lazy(() =>
       async (c) => {
         const sessionID = c.req.valid("param").sessionID
         const body = c.req.valid("json")
-        await SessionPrompt.command({
-          sessionID,
-          messageID: body.messageID,
-          model: body.providerID + "/" + body.modelID,
-          command: Command.Default.INIT,
-          arguments: "",
-        })
+        await AppRuntime.runPromise(
+          SessionPrompt.Service.use((svc) =>
+            svc.command({
+              sessionID,
+              messageID: body.messageID,
+              model: body.providerID + "/" + body.modelID,
+              command: Command.Default.INIT,
+              arguments: "",
+            }),
+          ),
+        )
         return c.json(true)
       },
     )
@@ -371,14 +380,14 @@ export const SessionRoutes = lazy(() =>
       validator(
         "param",
         z.object({
-          sessionID: Session.fork.schema.shape.sessionID,
+          sessionID: Session.ForkInput.shape.sessionID,
         }),
       ),
-      validator("json", Session.fork.schema.omit({ sessionID: true })),
+      validator("json", Session.ForkInput.omit({ sessionID: true })),
       async (c) => {
         const sessionID = c.req.valid("param").sessionID
         const body = c.req.valid("json")
-        const result = await Session.fork({ ...body, sessionID })
+        const result = await AppRuntime.runPromise(Session.Service.use((svc) => svc.fork({ ...body, sessionID })))
         return c.json(result)
       },
     )
@@ -407,7 +416,7 @@ export const SessionRoutes = lazy(() =>
         }),
       ),
       async (c) => {
-        await SessionPrompt.cancel(c.req.valid("param").sessionID)
+        await AppRuntime.runPromise(SessionPrompt.Service.use((svc) => svc.cancel(c.req.valid("param").sessionID)))
         return c.json(true)
       },
     )
@@ -657,15 +666,14 @@ export const SessionRoutes = lazy(() =>
       async (c) => {
         const query = c.req.valid("query")
         const sessionID = c.req.valid("param").sessionID
-        if (query.limit === undefined) {
-          await Session.get(sessionID)
-          const messages = await Session.messages({ sessionID })
-          return c.json(messages)
-        }
-
-        if (query.limit === 0) {
-          await Session.get(sessionID)
-          const messages = await Session.messages({ sessionID })
+        if (query.limit === undefined || query.limit === 0) {
+          const messages = await AppRuntime.runPromise(
+            Effect.gen(function* () {
+              const session = yield* Session.Service
+              yield* session.get(sessionID)
+              return yield* session.messages({ sessionID })
+            }),
+          )
           return c.json(messages)
         }
 
@@ -793,11 +801,15 @@ export const SessionRoutes = lazy(() =>
       ),
       async (c) => {
         const params = c.req.valid("param")
-        await Session.removePart({
-          sessionID: params.sessionID,
-          messageID: params.messageID,
-          partID: params.partID,
-        })
+        await AppRuntime.runPromise(
+          Session.Service.use((svc) =>
+            svc.removePart({
+              sessionID: params.sessionID,
+              messageID: params.messageID,
+              partID: params.partID,
+            }),
+          ),
+        )
         return c.json(true)
       },
     )
@@ -835,7 +847,7 @@ export const SessionRoutes = lazy(() =>
             `Part mismatch: body.id='${body.id}' vs partID='${params.partID}', body.messageID='${body.messageID}' vs messageID='${params.messageID}', body.sessionID='${body.sessionID}' vs sessionID='${params.sessionID}'`,
           )
         }
-        const part = await Session.updatePart(body)
+        const part = await AppRuntime.runPromise(Session.Service.use((svc) => svc.updatePart(body)))
         return c.json(part)
       },
     )
@@ -875,7 +887,9 @@ export const SessionRoutes = lazy(() =>
         return stream(c, async (stream) => {
           const sessionID = c.req.valid("param").sessionID
           const body = c.req.valid("json")
-          const msg = await SessionPrompt.prompt({ ...body, sessionID })
+          const msg = await AppRuntime.runPromise(
+            SessionPrompt.Service.use((svc) => svc.prompt({ ...body, sessionID })),
+          )
           stream.write(JSON.stringify(msg))
         })
       },
@@ -904,7 +918,7 @@ export const SessionRoutes = lazy(() =>
       async (c) => {
         const sessionID = c.req.valid("param").sessionID
         const body = c.req.valid("json")
-        SessionPrompt.prompt({ ...body, sessionID }).catch((err) => {
+        AppRuntime.runPromise(SessionPrompt.Service.use((svc) => svc.prompt({ ...body, sessionID }))).catch((err) => {
           log.error("prompt_async failed", { sessionID, error: err })
           Bus.publish(Session.Event.Error, {
             sessionID,
@@ -948,7 +962,7 @@ export const SessionRoutes = lazy(() =>
       async (c) => {
         const sessionID = c.req.valid("param").sessionID
         const body = c.req.valid("json")
-        const msg = await SessionPrompt.command({ ...body, sessionID })
+        const msg = await AppRuntime.runPromise(SessionPrompt.Service.use((svc) => svc.command({ ...body, sessionID })))
         return c.json(msg)
       },
     )
@@ -980,7 +994,7 @@ export const SessionRoutes = lazy(() =>
       async (c) => {
         const sessionID = c.req.valid("param").sessionID
         const body = c.req.valid("json")
-        const msg = await SessionPrompt.shell({ ...body, sessionID })
+        const msg = await AppRuntime.runPromise(SessionPrompt.Service.use((svc) => svc.shell({ ...body, sessionID })))
         return c.json(msg)
       },
     )

+ 2 - 1
packages/opencode/src/server/instance/tui.ts

@@ -4,6 +4,7 @@ import z from "zod"
 import { Bus } from "../../bus"
 import { Session } from "../../session"
 import { TuiEvent } from "@/cli/cmd/tui/event"
+import { AppRuntime } from "@/effect/app-runtime"
 import { AsyncQueue } from "../../util/queue"
 import { errors } from "../error"
 import { lazy } from "../../util/lazy"
@@ -370,7 +371,7 @@ export const TuiRoutes = lazy(() =>
       validator("json", TuiEvent.SessionSelect.properties),
       async (c) => {
         const { sessionID } = c.req.valid("json")
-        await Session.get(sessionID)
+        await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID)))
         await Bus.publish(TuiEvent.SessionSelect, { sessionID })
         return c.json(true)
       },

+ 24 - 65
packages/opencode/src/session/index.ts

@@ -19,7 +19,6 @@ import { updateSchema } from "../util/update-schema"
 import { MessageV2 } from "./message-v2"
 import { Instance } from "../project/instance"
 import { InstanceState } from "@/effect/instance-state"
-import { fn } from "@/util/fn"
 import { Snapshot } from "@/snapshot"
 import { ProjectID } from "../project/schema"
 import { WorkspaceID } from "../control-plane/schema"
@@ -29,7 +28,6 @@ import type { Provider } from "@/provider/provider"
 import { Permission } from "@/permission"
 import { Global } from "@/global"
 import { Effect, Layer, Option, Context } from "effect"
-import { makeRuntime } from "@/effect/run-service"
 
 export namespace Session {
   const log = Log.create({ service: "session" })
@@ -179,6 +177,30 @@ export namespace Session {
   })
   export type GlobalInfo = z.output<typeof GlobalInfo>
 
+  export const CreateInput = z
+    .object({
+      parentID: SessionID.zod.optional(),
+      title: z.string().optional(),
+      permission: Info.shape.permission,
+      workspaceID: WorkspaceID.zod.optional(),
+    })
+    .optional()
+  export type CreateInput = z.output<typeof CreateInput>
+
+  export const ForkInput = z.object({ sessionID: SessionID.zod, messageID: MessageID.zod.optional() })
+  export const GetInput = SessionID.zod
+  export const ChildrenInput = SessionID.zod
+  export const RemoveInput = SessionID.zod
+  export const SetTitleInput = z.object({ sessionID: SessionID.zod, title: z.string() })
+  export const SetArchivedInput = z.object({ sessionID: SessionID.zod, time: z.number().optional() })
+  export const SetPermissionInput = z.object({ sessionID: SessionID.zod, permission: Permission.Ruleset })
+  export const SetRevertInput = z.object({
+    sessionID: SessionID.zod,
+    revert: Info.shape.revert,
+    summary: Info.shape.summary,
+  })
+  export const MessagesInput = z.object({ sessionID: SessionID.zod, limit: z.number().optional() })
+
   export const Event = {
     Created: SyncEvent.define({
       type: "session.created",
@@ -682,48 +704,6 @@ export namespace Session {
 
   export const defaultLayer = layer.pipe(Layer.provide(Bus.layer), Layer.provide(Storage.defaultLayer))
 
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export const create = fn(
-    z
-      .object({
-        parentID: SessionID.zod.optional(),
-        title: z.string().optional(),
-        permission: Info.shape.permission,
-        workspaceID: WorkspaceID.zod.optional(),
-      })
-      .optional(),
-    (input) => runPromise((svc) => svc.create(input)),
-  )
-
-  export const fork = fn(z.object({ sessionID: SessionID.zod, messageID: MessageID.zod.optional() }), (input) =>
-    runPromise((svc) => svc.fork(input)),
-  )
-
-  export const get = fn(SessionID.zod, (id) => runPromise((svc) => svc.get(id)))
-
-  export const setTitle = fn(z.object({ sessionID: SessionID.zod, title: z.string() }), (input) =>
-    runPromise((svc) => svc.setTitle(input)),
-  )
-
-  export const setArchived = fn(z.object({ sessionID: SessionID.zod, time: z.number().optional() }), (input) =>
-    runPromise((svc) => svc.setArchived(input)),
-  )
-
-  export const setPermission = fn(z.object({ sessionID: SessionID.zod, permission: Permission.Ruleset }), (input) =>
-    runPromise((svc) => svc.setPermission(input)),
-  )
-
-  export const setRevert = fn(
-    z.object({ sessionID: SessionID.zod, revert: Info.shape.revert, summary: Info.shape.summary }),
-    (input) =>
-      runPromise((svc) => svc.setRevert({ sessionID: input.sessionID, revert: input.revert, summary: input.summary })),
-  )
-
-  export const messages = fn(z.object({ sessionID: SessionID.zod, limit: z.number().optional() }), (input) =>
-    runPromise((svc) => svc.messages(input)),
-  )
-
   export function* list(input?: {
     directory?: string
     workspaceID?: WorkspaceID
@@ -835,25 +815,4 @@ export namespace Session {
       yield { ...fromRow(row), project }
     }
   }
-
-  export const children = fn(SessionID.zod, (id) => runPromise((svc) => svc.children(id)))
-  export const remove = fn(SessionID.zod, (id) => runPromise((svc) => svc.remove(id)))
-  export async function updateMessage<T extends MessageV2.Info>(msg: T): Promise<T> {
-    MessageV2.Info.parse(msg)
-    return runPromise((svc) => svc.updateMessage(msg))
-  }
-
-  export const removeMessage = fn(z.object({ sessionID: SessionID.zod, messageID: MessageID.zod }), (input) =>
-    runPromise((svc) => svc.removeMessage(input)),
-  )
-
-  export const removePart = fn(
-    z.object({ sessionID: SessionID.zod, messageID: MessageID.zod, partID: PartID.zod }),
-    (input) => runPromise((svc) => svc.removePart(input)),
-  )
-
-  export async function updatePart<T extends MessageV2.Part>(part: T): Promise<T> {
-    MessageV2.Part.parse(part)
-    return runPromise((svc) => svc.updatePart(part))
-  }
 }

+ 2 - 30
packages/opencode/src/session/prompt.ts

@@ -46,7 +46,6 @@ import { Process } from "@/util/process"
 import { Cause, Effect, Exit, Layer, Option, Scope, Context } from "effect"
 import { EffectLogger } from "@/effect/logger"
 import { InstanceState } from "@/effect/instance-state"
-import { attach, makeRuntime } from "@/effect/run-service"
 import { TaskTool, type TaskPromptOps } from "@/tool/task"
 import { SessionRunState } from "./run-state"
 
@@ -108,9 +107,8 @@ export namespace SessionPrompt {
 
       const run = {
         promise: <A, E>(effect: Effect.Effect<A, E>) =>
-          Effect.runPromise(attach(effect).pipe(Effect.provide(EffectLogger.layer))),
-        fork: <A, E>(effect: Effect.Effect<A, E>) =>
-          Effect.runFork(attach(effect).pipe(Effect.provide(EffectLogger.layer))),
+          Effect.runPromise(effect.pipe(Effect.provide(EffectLogger.layer))),
+        fork: <A, E>(effect: Effect.Effect<A, E>) => Effect.runFork(effect.pipe(Effect.provide(EffectLogger.layer))),
       }
 
       const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) {
@@ -1709,8 +1707,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
       ),
     ),
   )
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
   export const PromptInput = z.object({
     sessionID: SessionID.zod,
     messageID: MessageID.zod.optional(),
@@ -1778,26 +1774,10 @@ NOTE: At any point in time through this workflow you should feel free to ask the
   })
   export type PromptInput = z.infer<typeof PromptInput>
 
-  export async function prompt(input: PromptInput) {
-    return runPromise((svc) => svc.prompt(PromptInput.parse(input)))
-  }
-
-  export async function resolvePromptParts(template: string) {
-    return runPromise((svc) => svc.resolvePromptParts(z.string().parse(template)))
-  }
-
-  export async function cancel(sessionID: SessionID) {
-    return runPromise((svc) => svc.cancel(SessionID.zod.parse(sessionID)))
-  }
-
   export const LoopInput = z.object({
     sessionID: SessionID.zod,
   })
 
-  export async function loop(input: z.infer<typeof LoopInput>) {
-    return runPromise((svc) => svc.loop(LoopInput.parse(input)))
-  }
-
   export const ShellInput = z.object({
     sessionID: SessionID.zod,
     messageID: MessageID.zod.optional(),
@@ -1812,10 +1792,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
   })
   export type ShellInput = z.infer<typeof ShellInput>
 
-  export async function shell(input: ShellInput) {
-    return runPromise((svc) => svc.shell(ShellInput.parse(input)))
-  }
-
   export const CommandInput = z.object({
     messageID: MessageID.zod.optional(),
     sessionID: SessionID.zod,
@@ -1839,10 +1815,6 @@ NOTE: At any point in time through this workflow you should feel free to ask the
   })
   export type CommandInput = z.infer<typeof CommandInput>
 
-  export async function command(input: CommandInput) {
-    return runPromise((svc) => svc.command(CommandInput.parse(input)))
-  }
-
   /** @internal Exported for testing */
   export function createStructuredOutputTool(input: {
     schema: Record<string, any>

+ 2 - 2
packages/opencode/src/share/session.ts

@@ -8,7 +8,7 @@ import { ShareNext } from "./share-next"
 
 export namespace SessionShare {
   export interface Interface {
-    readonly create: (input?: Parameters<typeof Session.create>[0]) => Effect.Effect<Session.Info>
+    readonly create: (input?: Session.CreateInput) => Effect.Effect<Session.Info>
     readonly share: (sessionID: SessionID) => Effect.Effect<{ url: string }, unknown>
     readonly unshare: (sessionID: SessionID) => Effect.Effect<void, unknown>
   }
@@ -38,7 +38,7 @@ export namespace SessionShare {
         yield* Effect.sync(() => SyncEvent.run(Session.Event.Updated, { sessionID, info: { share: { url: null } } }))
       })
 
-      const create = Effect.fn("SessionShare.create")(function* (input?: Parameters<typeof Session.create>[0]) {
+      const create = Effect.fn("SessionShare.create")(function* (input?: Session.CreateInput) {
         const result = yield* session.create(input)
         if (result.parentID) return result
         const conf = yield* cfg.get()

+ 29 - 13
packages/opencode/test/server/global-session-list.test.ts

@@ -1,27 +1,43 @@
 import { describe, expect, test } from "bun:test"
+import { Effect } from "effect"
+import z from "zod"
 import { Instance } from "../../src/project/instance"
 import { Project } from "../../src/project/project"
-import { Session } from "../../src/session"
+import { Session as SessionNs } from "../../src/session"
 import { Log } from "../../src/util/log"
 import { tmpdir } from "../fixture/fixture"
 
 Log.init({ print: false })
 
-describe("Session.listGlobal", () => {
+function run<A, E>(fx: Effect.Effect<A, E, SessionNs.Service>) {
+  return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer)))
+}
+
+const svc = {
+  ...SessionNs,
+  create(input?: SessionNs.CreateInput) {
+    return run(SessionNs.Service.use((svc) => svc.create(input)))
+  },
+  setArchived(input: z.output<typeof SessionNs.SetArchivedInput>) {
+    return run(SessionNs.Service.use((svc) => svc.setArchived(input)))
+  },
+}
+
+describe("session.listGlobal", () => {
   test("lists sessions across projects with project metadata", async () => {
     await using first = await tmpdir({ git: true })
     await using second = await tmpdir({ git: true })
 
     const firstSession = await Instance.provide({
       directory: first.path,
-      fn: async () => Session.create({ title: "first-session" }),
+      fn: async () => svc.create({ title: "first-session" }),
     })
     const secondSession = await Instance.provide({
       directory: second.path,
-      fn: async () => Session.create({ title: "second-session" }),
+      fn: async () => svc.create({ title: "second-session" }),
     })
 
-    const sessions = [...Session.listGlobal({ limit: 200 })]
+    const sessions = [...svc.listGlobal({ limit: 200 })]
     const ids = sessions.map((session) => session.id)
 
     expect(ids).toContain(firstSession.id)
@@ -44,20 +60,20 @@ describe("Session.listGlobal", () => {
 
     const archived = await Instance.provide({
       directory: tmp.path,
-      fn: async () => Session.create({ title: "archived-session" }),
+      fn: async () => svc.create({ title: "archived-session" }),
     })
 
     await Instance.provide({
       directory: tmp.path,
-      fn: async () => Session.setArchived({ sessionID: archived.id, time: Date.now() }),
+      fn: async () => svc.setArchived({ sessionID: archived.id, time: Date.now() }),
     })
 
-    const sessions = [...Session.listGlobal({ limit: 200 })]
+    const sessions = [...svc.listGlobal({ limit: 200 })]
     const ids = sessions.map((session) => session.id)
 
     expect(ids).not.toContain(archived.id)
 
-    const allSessions = [...Session.listGlobal({ limit: 200, archived: true })]
+    const allSessions = [...svc.listGlobal({ limit: 200, archived: true })]
     const allIds = allSessions.map((session) => session.id)
 
     expect(allIds).toContain(archived.id)
@@ -68,19 +84,19 @@ describe("Session.listGlobal", () => {
 
     const first = await Instance.provide({
       directory: tmp.path,
-      fn: async () => Session.create({ title: "page-one" }),
+      fn: async () => svc.create({ title: "page-one" }),
     })
     await new Promise((resolve) => setTimeout(resolve, 5))
     const second = await Instance.provide({
       directory: tmp.path,
-      fn: async () => Session.create({ title: "page-two" }),
+      fn: async () => svc.create({ title: "page-two" }),
     })
 
-    const page = [...Session.listGlobal({ directory: tmp.path, limit: 1 })]
+    const page = [...svc.listGlobal({ directory: tmp.path, limit: 1 })]
     expect(page.length).toBe(1)
     expect(page[0].id).toBe(second.id)
 
-    const next = [...Session.listGlobal({ directory: tmp.path, limit: 10, cursor: page[0].time.updated })]
+    const next = [...svc.listGlobal({ directory: tmp.path, limit: 10, cursor: page[0].time.updated })]
     const ids = next.map((session) => session.id)
 
     expect(ids).toContain(first.id)

+ 20 - 8
packages/opencode/test/server/session-actions.test.ts

@@ -1,36 +1,48 @@
-import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"
+import { afterEach, describe, expect, mock, test } from "bun:test"
 import { Effect } from "effect"
 import { Instance } from "../../src/project/instance"
 import { Server } from "../../src/server/server"
-import { Session } from "../../src/session"
-import { SessionPrompt } from "../../src/session/prompt"
+import { Session as SessionNs } from "../../src/session"
+import type { SessionID } from "../../src/session/schema"
 import { Log } from "../../src/util/log"
 import { tmpdir } from "../fixture/fixture"
 
 Log.init({ print: false })
 
+function run<A, E>(fx: Effect.Effect<A, E, SessionNs.Service>) {
+  return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer)))
+}
+
+const svc = {
+  ...SessionNs,
+  create(input?: SessionNs.CreateInput) {
+    return run(SessionNs.Service.use((svc) => svc.create(input)))
+  },
+  remove(id: SessionID) {
+    return run(SessionNs.Service.use((svc) => svc.remove(id)))
+  },
+}
+
 afterEach(async () => {
   mock.restore()
   await Instance.disposeAll()
 })
 
 describe("session action routes", () => {
-  test("abort route calls SessionPrompt.cancel", async () => {
+  test("abort route returns success", async () => {
     await using tmp = await tmpdir({ git: true })
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const session = await Session.create({})
-        const cancel = spyOn(SessionPrompt, "cancel").mockResolvedValue()
+        const session = await svc.create({})
         const app = Server.Default().app
 
         const res = await app.request(`/session/${session.id}/abort`, { method: "POST" })
 
         expect(res.status).toBe(200)
         expect(await res.json()).toBe(true)
-        expect(cancel).toHaveBeenCalledWith(session.id)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })

+ 29 - 17
packages/opencode/test/server/session-list.test.ts

@@ -1,30 +1,42 @@
 import { afterEach, describe, expect, test } from "bun:test"
+import { Effect } from "effect"
 import { Instance } from "../../src/project/instance"
-import { Session } from "../../src/session"
+import { Session as SessionNs } from "../../src/session"
 import { Log } from "../../src/util/log"
 import { tmpdir } from "../fixture/fixture"
 
 Log.init({ print: false })
 
+function run<A, E>(fx: Effect.Effect<A, E, SessionNs.Service>) {
+  return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer)))
+}
+
+const svc = {
+  ...SessionNs,
+  create(input?: SessionNs.CreateInput) {
+    return run(SessionNs.Service.use((svc) => svc.create(input)))
+  },
+}
+
 afterEach(async () => {
   await Instance.disposeAll()
 })
 
-describe("Session.list", () => {
+describe("session.list", () => {
   test("filters by directory", async () => {
     await using tmp = await tmpdir({ git: true })
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const first = await Session.create({})
+        const first = await svc.create({})
 
         await using other = await tmpdir({ git: true })
         const second = await Instance.provide({
           directory: other.path,
-          fn: async () => Session.create({}),
+          fn: async () => svc.create({}),
         })
 
-        const sessions = [...Session.list({ directory: tmp.path })]
+        const sessions = [...svc.list({ directory: tmp.path })]
         const ids = sessions.map((s) => s.id)
 
         expect(ids).toContain(first.id)
@@ -38,10 +50,10 @@ describe("Session.list", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const root = await Session.create({ title: "root-session" })
-        const child = await Session.create({ title: "child-session", parentID: root.id })
+        const root = await svc.create({ title: "root-session" })
+        const child = await svc.create({ title: "child-session", parentID: root.id })
 
-        const sessions = [...Session.list({ roots: true })]
+        const sessions = [...svc.list({ roots: true })]
         const ids = sessions.map((s) => s.id)
 
         expect(ids).toContain(root.id)
@@ -55,10 +67,10 @@ describe("Session.list", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const session = await Session.create({ title: "new-session" })
+        const session = await svc.create({ title: "new-session" })
         const futureStart = Date.now() + 86400000
 
-        const sessions = [...Session.list({ start: futureStart })]
+        const sessions = [...svc.list({ start: futureStart })]
         expect(sessions.length).toBe(0)
       },
     })
@@ -69,10 +81,10 @@ describe("Session.list", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        await Session.create({ title: "unique-search-term-abc" })
-        await Session.create({ title: "other-session-xyz" })
+        await svc.create({ title: "unique-search-term-abc" })
+        await svc.create({ title: "other-session-xyz" })
 
-        const sessions = [...Session.list({ search: "unique-search" })]
+        const sessions = [...svc.list({ search: "unique-search" })]
         const titles = sessions.map((s) => s.title)
 
         expect(titles).toContain("unique-search-term-abc")
@@ -86,11 +98,11 @@ describe("Session.list", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        await Session.create({ title: "session-1" })
-        await Session.create({ title: "session-2" })
-        await Session.create({ title: "session-3" })
+        await svc.create({ title: "session-1" })
+        await svc.create({ title: "session-2" })
+        await svc.create({ title: "session-3" })
 
-        const sessions = [...Session.list({ limit: 2 })]
+        const sessions = [...svc.list({ limit: 2 })]
         expect(sessions.length).toBe(2)
       },
     })

+ 32 - 11
packages/opencode/test/server/session-messages.test.ts

@@ -1,7 +1,8 @@
 import { afterEach, describe, expect, test } from "bun:test"
+import { Effect } from "effect"
 import { Instance } from "../../src/project/instance"
 import { Server } from "../../src/server/server"
-import { Session } from "../../src/session"
+import { Session as SessionNs } from "../../src/session"
 import { MessageV2 } from "../../src/session/message-v2"
 import { MessageID, PartID, type SessionID } from "../../src/session/schema"
 import { Log } from "../../src/util/log"
@@ -9,6 +10,26 @@ import { tmpdir } from "../fixture/fixture"
 
 Log.init({ print: false })
 
+function run<A, E>(fx: Effect.Effect<A, E, SessionNs.Service>) {
+  return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer)))
+}
+
+const svc = {
+  ...SessionNs,
+  create(input?: SessionNs.CreateInput) {
+    return run(SessionNs.Service.use((svc) => svc.create(input)))
+  },
+  remove(id: SessionID) {
+    return run(SessionNs.Service.use((svc) => svc.remove(id)))
+  },
+  updateMessage<T extends MessageV2.Info>(msg: T) {
+    return run(SessionNs.Service.use((svc) => svc.updateMessage(msg)))
+  },
+  updatePart<T extends MessageV2.Part>(part: T) {
+    return run(SessionNs.Service.use((svc) => svc.updatePart(part)))
+  },
+}
+
 afterEach(async () => {
   await Instance.disposeAll()
 })
@@ -30,7 +51,7 @@ async function fill(sessionID: SessionID, count: number, time = (i: number) => D
   for (let i = 0; i < count; i++) {
     const id = MessageID.ascending()
     ids.push(id)
-    await Session.updateMessage({
+    await svc.updateMessage({
       id,
       sessionID,
       role: "user",
@@ -40,7 +61,7 @@ async function fill(sessionID: SessionID, count: number, time = (i: number) => D
       tools: {},
       mode: "",
     } as unknown as MessageV2.Info)
-    await Session.updatePart({
+    await svc.updatePart({
       id: PartID.ascending(),
       sessionID,
       messageID: id,
@@ -58,7 +79,7 @@ describe("session messages endpoint", () => {
       Instance.provide({
         directory: tmp.path,
         fn: async () => {
-          const session = await Session.create({})
+          const session = await svc.create({})
           const ids = await fill(session.id, 5)
           const app = Server.Default().app
 
@@ -75,7 +96,7 @@ describe("session messages endpoint", () => {
           const bBody = (await b.json()) as MessageV2.WithParts[]
           expect(bBody.map((item) => item.info.id)).toEqual(ids.slice(-4, -2))
 
-          await Session.remove(session.id)
+          await svc.remove(session.id)
         },
       }),
     )
@@ -87,7 +108,7 @@ describe("session messages endpoint", () => {
       Instance.provide({
         directory: tmp.path,
         fn: async () => {
-          const session = await Session.create({})
+          const session = await svc.create({})
           const ids = await fill(session.id, 3)
           const app = Server.Default().app
 
@@ -96,7 +117,7 @@ describe("session messages endpoint", () => {
           const body = (await res.json()) as MessageV2.WithParts[]
           expect(body.map((item) => item.info.id)).toEqual(ids)
 
-          await Session.remove(session.id)
+          await svc.remove(session.id)
         },
       }),
     )
@@ -108,7 +129,7 @@ describe("session messages endpoint", () => {
       Instance.provide({
         directory: tmp.path,
         fn: async () => {
-          const session = await Session.create({})
+          const session = await svc.create({})
           const app = Server.Default().app
 
           const bad = await app.request(`/session/${session.id}/message?limit=2&before=bad`)
@@ -117,7 +138,7 @@ describe("session messages endpoint", () => {
           const miss = await app.request(`/session/ses_missing/message?limit=2`)
           expect(miss.status).toBe(404)
 
-          await Session.remove(session.id)
+          await svc.remove(session.id)
         },
       }),
     )
@@ -129,7 +150,7 @@ describe("session messages endpoint", () => {
       Instance.provide({
         directory: tmp.path,
         fn: async () => {
-          const session = await Session.create({})
+          const session = await svc.create({})
           await fill(session.id, 520)
           const app = Server.Default().app
 
@@ -138,7 +159,7 @@ describe("session messages endpoint", () => {
           const body = (await res.json()) as MessageV2.WithParts[]
           expect(body).toHaveLength(510)
 
-          await Session.remove(session.id)
+          await svc.remove(session.id)
         },
       }),
     )

+ 19 - 3
packages/opencode/test/server/session-select.test.ts

@@ -1,5 +1,7 @@
 import { afterEach, describe, expect, test } from "bun:test"
-import { Session } from "../../src/session"
+import { Effect } from "effect"
+import { Session as SessionNs } from "../../src/session"
+import type { SessionID } from "../../src/session/schema"
 import { Log } from "../../src/util/log"
 import { Instance } from "../../src/project/instance"
 import { Server } from "../../src/server/server"
@@ -7,6 +9,20 @@ import { tmpdir } from "../fixture/fixture"
 
 Log.init({ print: false })
 
+function run<A, E>(fx: Effect.Effect<A, E, SessionNs.Service>) {
+  return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer)))
+}
+
+const svc = {
+  ...SessionNs,
+  create(input?: SessionNs.CreateInput) {
+    return run(SessionNs.Service.use((svc) => svc.create(input)))
+  },
+  remove(id: SessionID) {
+    return run(SessionNs.Service.use((svc) => svc.remove(id)))
+  },
+}
+
 afterEach(async () => {
   await Instance.disposeAll()
 })
@@ -18,7 +34,7 @@ describe("tui.selectSession endpoint", () => {
       directory: tmp.path,
       fn: async () => {
         // #given
-        const session = await Session.create({})
+        const session = await svc.create({})
 
         // #when
         const app = Server.Default().app
@@ -33,7 +49,7 @@ describe("tui.selectSession endpoint", () => {
         const body = await response.json()
         expect(body).toBe(true)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })

+ 91 - 70
packages/opencode/test/session/compaction.test.ts

@@ -3,6 +3,7 @@ import { APICallError } from "ai"
 import { Cause, Effect, Exit, Layer, ManagedRuntime } from "effect"
 import * as Stream from "effect/Stream"
 import path from "path"
+import z from "zod"
 import { Bus } from "../../src/bus"
 import { Config } from "../../src/config/config"
 import { Agent } from "../../src/agent/agent"
@@ -14,7 +15,7 @@ import { Log } from "../../src/util/log"
 import { Permission } from "../../src/permission"
 import { Plugin } from "../../src/plugin"
 import { provideTmpdirInstance, tmpdir } from "../fixture/fixture"
-import { Session } from "../../src/session"
+import { Session as SessionNs } from "../../src/session"
 import { MessageV2 } from "../../src/session/message-v2"
 import { MessageID, PartID, SessionID } from "../../src/session/schema"
 import { SessionStatus } from "../../src/session/status"
@@ -29,6 +30,26 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
 
 Log.init({ print: false })
 
+function run<A, E>(fx: Effect.Effect<A, E, SessionNs.Service>) {
+  return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer)))
+}
+
+const svc = {
+  ...SessionNs,
+  create(input?: SessionNs.CreateInput) {
+    return run(SessionNs.Service.use((svc) => svc.create(input)))
+  },
+  messages(input: z.output<typeof SessionNs.MessagesInput>) {
+    return run(SessionNs.Service.use((svc) => svc.messages(input)))
+  },
+  updateMessage<T extends MessageV2.Info>(msg: T) {
+    return run(SessionNs.Service.use((svc) => svc.updateMessage(msg)))
+  },
+  updatePart<T extends MessageV2.Part>(part: T) {
+    return run(SessionNs.Service.use((svc) => svc.updatePart(part)))
+  },
+}
+
 const summary = Layer.succeed(
   SessionSummary.Service,
   SessionSummary.Service.of({
@@ -80,7 +101,7 @@ function createModel(opts: {
 const wide = () => ProviderTest.fake({ model: createModel({ context: 100_000, output: 32_000 }) })
 
 async function user(sessionID: SessionID, text: string) {
-  const msg = await Session.updateMessage({
+  const msg = await svc.updateMessage({
     id: MessageID.ascending(),
     role: "user",
     sessionID,
@@ -88,7 +109,7 @@ async function user(sessionID: SessionID, text: string) {
     model: ref,
     time: { created: Date.now() },
   })
-  await Session.updatePart({
+  await svc.updatePart({
     id: PartID.ascending(),
     messageID: msg.id,
     sessionID,
@@ -119,12 +140,12 @@ async function assistant(sessionID: SessionID, parentID: MessageID, root: string
     time: { created: Date.now() },
     finish: "end_turn",
   }
-  await Session.updateMessage(msg)
+  await svc.updateMessage(msg)
   return msg
 }
 
 async function tool(sessionID: SessionID, messageID: MessageID, tool: string, output: string) {
-  return Session.updatePart({
+  return svc.updatePart({
     id: PartID.ascending(),
     messageID,
     sessionID,
@@ -171,7 +192,7 @@ function runtime(result: "continue" | "compact", plugin = Plugin.defaultLayer, p
   return ManagedRuntime.make(
     Layer.mergeAll(SessionCompaction.layer, bus).pipe(
       Layer.provide(provider.layer),
-      Layer.provide(Session.defaultLayer),
+      Layer.provide(SessionNs.defaultLayer),
       Layer.provide(layer(result)),
       Layer.provide(Agent.defaultLayer),
       Layer.provide(plugin),
@@ -191,9 +212,9 @@ const deps = Layer.mergeAll(
 )
 
 const env = Layer.mergeAll(
-  Session.defaultLayer,
+  SessionNs.defaultLayer,
   CrossSpawnSpawner.defaultLayer,
-  SessionCompaction.layer.pipe(Layer.provide(Session.defaultLayer), Layer.provideMerge(deps)),
+  SessionCompaction.layer.pipe(Layer.provide(SessionNs.defaultLayer), Layer.provideMerge(deps)),
 )
 
 const it = testEffect(env)
@@ -227,7 +248,7 @@ function liveRuntime(layer: Layer.Layer<LLM.Service>, provider = ProviderTest.fa
   return ManagedRuntime.make(
     Layer.mergeAll(SessionCompaction.layer.pipe(Layer.provide(processor)), processor, bus, status).pipe(
       Layer.provide(provider.layer),
-      Layer.provide(Session.defaultLayer),
+      Layer.provide(SessionNs.defaultLayer),
       Layer.provide(Snapshot.defaultLayer),
       Layer.provide(layer),
       Layer.provide(Permission.defaultLayer),
@@ -467,9 +488,9 @@ describe("session.compaction.create", () => {
     provideTmpdirInstance(() =>
       Effect.gen(function* () {
         const compact = yield* SessionCompaction.Service
-        const session = yield* Session.Service
+        const ssn = yield* SessionNs.Service
 
-        const info = yield* session.create({})
+        const info = yield* ssn.create({})
 
         yield* compact.create({
           sessionID: info.id,
@@ -479,7 +500,7 @@ describe("session.compaction.create", () => {
           overflow: true,
         })
 
-        const msgs = yield* session.messages({ sessionID: info.id })
+        const msgs = yield* ssn.messages({ sessionID: info.id })
         expect(msgs).toHaveLength(1)
         expect(msgs[0].info.role).toBe("user")
         expect(msgs[0].parts).toHaveLength(1)
@@ -499,9 +520,9 @@ describe("session.compaction.prune", () => {
     provideTmpdirInstance((dir) =>
       Effect.gen(function* () {
         const compact = yield* SessionCompaction.Service
-        const session = yield* Session.Service
-        const info = yield* session.create({})
-        const a = yield* session.updateMessage({
+        const ssn = yield* SessionNs.Service
+        const info = yield* ssn.create({})
+        const a = yield* ssn.updateMessage({
           id: MessageID.ascending(),
           role: "user",
           sessionID: info.id,
@@ -509,7 +530,7 @@ describe("session.compaction.prune", () => {
           model: ref,
           time: { created: Date.now() },
         })
-        yield* session.updatePart({
+        yield* ssn.updatePart({
           id: PartID.ascending(),
           messageID: a.id,
           sessionID: info.id,
@@ -536,8 +557,8 @@ describe("session.compaction.prune", () => {
           time: { created: Date.now() },
           finish: "end_turn",
         }
-        yield* session.updateMessage(b)
-        yield* session.updatePart({
+        yield* ssn.updateMessage(b)
+        yield* ssn.updatePart({
           id: PartID.ascending(),
           messageID: b.id,
           sessionID: info.id,
@@ -554,7 +575,7 @@ describe("session.compaction.prune", () => {
           },
         })
         for (const text of ["second", "third"]) {
-          const msg = yield* session.updateMessage({
+          const msg = yield* ssn.updateMessage({
             id: MessageID.ascending(),
             role: "user",
             sessionID: info.id,
@@ -562,7 +583,7 @@ describe("session.compaction.prune", () => {
             model: ref,
             time: { created: Date.now() },
           })
-          yield* session.updatePart({
+          yield* ssn.updatePart({
             id: PartID.ascending(),
             messageID: msg.id,
             sessionID: info.id,
@@ -573,7 +594,7 @@ describe("session.compaction.prune", () => {
 
         yield* compact.prune({ sessionID: info.id })
 
-        const msgs = yield* session.messages({ sessionID: info.id })
+        const msgs = yield* ssn.messages({ sessionID: info.id })
         const part = msgs.flatMap((msg) => msg.parts).find((part) => part.type === "tool")
         expect(part?.type).toBe("tool")
         expect(part?.state.status).toBe("completed")
@@ -589,9 +610,9 @@ describe("session.compaction.prune", () => {
     provideTmpdirInstance((dir) =>
       Effect.gen(function* () {
         const compact = yield* SessionCompaction.Service
-        const session = yield* Session.Service
-        const info = yield* session.create({})
-        const a = yield* session.updateMessage({
+        const ssn = yield* SessionNs.Service
+        const info = yield* ssn.create({})
+        const a = yield* ssn.updateMessage({
           id: MessageID.ascending(),
           role: "user",
           sessionID: info.id,
@@ -599,7 +620,7 @@ describe("session.compaction.prune", () => {
           model: ref,
           time: { created: Date.now() },
         })
-        yield* session.updatePart({
+        yield* ssn.updatePart({
           id: PartID.ascending(),
           messageID: a.id,
           sessionID: info.id,
@@ -626,8 +647,8 @@ describe("session.compaction.prune", () => {
           time: { created: Date.now() },
           finish: "end_turn",
         }
-        yield* session.updateMessage(b)
-        yield* session.updatePart({
+        yield* ssn.updateMessage(b)
+        yield* ssn.updatePart({
           id: PartID.ascending(),
           messageID: b.id,
           sessionID: info.id,
@@ -644,7 +665,7 @@ describe("session.compaction.prune", () => {
           },
         })
         for (const text of ["second", "third"]) {
-          const msg = yield* session.updateMessage({
+          const msg = yield* ssn.updateMessage({
             id: MessageID.ascending(),
             role: "user",
             sessionID: info.id,
@@ -652,7 +673,7 @@ describe("session.compaction.prune", () => {
             model: ref,
             time: { created: Date.now() },
           })
-          yield* session.updatePart({
+          yield* ssn.updatePart({
             id: PartID.ascending(),
             messageID: msg.id,
             sessionID: info.id,
@@ -663,7 +684,7 @@ describe("session.compaction.prune", () => {
 
         yield* compact.prune({ sessionID: info.id })
 
-        const msgs = yield* session.messages({ sessionID: info.id })
+        const msgs = yield* ssn.messages({ sessionID: info.id })
         const part = msgs.flatMap((msg) => msg.parts).find((part) => part.type === "tool")
         expect(part?.type).toBe("tool")
         if (part?.type === "tool" && part.state.status === "completed") {
@@ -680,12 +701,12 @@ describe("session.compaction.process", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const msg = await user(session.id, "hello")
         const reply = await assistant(session.id, msg.id, tmp.path)
         const rt = runtime("continue")
         try {
-          const msgs = await Session.messages({ sessionID: session.id })
+          const msgs = await svc.messages({ sessionID: session.id })
           await expect(
             rt.runPromise(
               SessionCompaction.Service.use((svc) =>
@@ -710,9 +731,9 @@ describe("session.compaction.process", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const msg = await user(session.id, "hello")
-        const msgs = await Session.messages({ sessionID: session.id })
+        const msgs = await svc.messages({ sessionID: session.id })
         const done = defer()
         let seen = false
         const rt = runtime("continue", Plugin.defaultLayer, wide())
@@ -760,11 +781,11 @@ describe("session.compaction.process", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const msg = await user(session.id, "hello")
         const rt = runtime("compact", Plugin.defaultLayer, wide())
         try {
-          const msgs = await Session.messages({ sessionID: session.id })
+          const msgs = await svc.messages({ sessionID: session.id })
           const result = await rt.runPromise(
             SessionCompaction.Service.use((svc) =>
               svc.process({
@@ -776,7 +797,7 @@ describe("session.compaction.process", () => {
             ),
           )
 
-          const summary = (await Session.messages({ sessionID: session.id })).find(
+          const summary = (await svc.messages({ sessionID: session.id })).find(
             (msg) => msg.info.role === "assistant" && msg.info.summary,
           )
 
@@ -798,11 +819,11 @@ describe("session.compaction.process", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const msg = await user(session.id, "hello")
         const rt = runtime("continue", Plugin.defaultLayer, wide())
         try {
-          const msgs = await Session.messages({ sessionID: session.id })
+          const msgs = await svc.messages({ sessionID: session.id })
           const result = await rt.runPromise(
             SessionCompaction.Service.use((svc) =>
               svc.process({
@@ -814,7 +835,7 @@ describe("session.compaction.process", () => {
             ),
           )
 
-          const all = await Session.messages({ sessionID: session.id })
+          const all = await svc.messages({ sessionID: session.id })
           const last = all.at(-1)
 
           expect(result).toBe("continue")
@@ -838,11 +859,11 @@ describe("session.compaction.process", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const msg = await user(session.id, "hello")
         const rt = runtime("continue", autocontinue(false), wide())
         try {
-          const msgs = await Session.messages({ sessionID: session.id })
+          const msgs = await svc.messages({ sessionID: session.id })
           const result = await rt.runPromise(
             SessionCompaction.Service.use((svc) =>
               svc.process({
@@ -854,7 +875,7 @@ describe("session.compaction.process", () => {
             ),
           )
 
-          const all = await Session.messages({ sessionID: session.id })
+          const all = await svc.messages({ sessionID: session.id })
           const last = all.at(-1)
 
           expect(result).toBe("continue")
@@ -881,10 +902,10 @@ describe("session.compaction.process", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         await user(session.id, "root")
         const replay = await user(session.id, "image")
-        await Session.updatePart({
+        await svc.updatePart({
           id: PartID.ascending(),
           messageID: replay.id,
           sessionID: session.id,
@@ -896,7 +917,7 @@ describe("session.compaction.process", () => {
         const msg = await user(session.id, "current")
         const rt = runtime("continue", Plugin.defaultLayer, wide())
         try {
-          const msgs = await Session.messages({ sessionID: session.id })
+          const msgs = await svc.messages({ sessionID: session.id })
           const result = await rt.runPromise(
             SessionCompaction.Service.use((svc) =>
               svc.process({
@@ -909,7 +930,7 @@ describe("session.compaction.process", () => {
             ),
           )
 
-          const last = (await Session.messages({ sessionID: session.id })).at(-1)
+          const last = (await svc.messages({ sessionID: session.id })).at(-1)
 
           expect(result).toBe("continue")
           expect(last?.info.role).toBe("user")
@@ -929,13 +950,13 @@ describe("session.compaction.process", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         await user(session.id, "earlier")
         const msg = await user(session.id, "current")
 
         const rt = runtime("continue", Plugin.defaultLayer, wide())
         try {
-          const msgs = await Session.messages({ sessionID: session.id })
+          const msgs = await svc.messages({ sessionID: session.id })
           const result = await rt.runPromise(
             SessionCompaction.Service.use((svc) =>
               svc.process({
@@ -948,7 +969,7 @@ describe("session.compaction.process", () => {
             ),
           )
 
-          const last = (await Session.messages({ sessionID: session.id })).at(-1)
+          const last = (await svc.messages({ sessionID: session.id })).at(-1)
 
           expect(result).toBe("continue")
           expect(last?.info.role).toBe("user")
@@ -989,9 +1010,9 @@ describe("session.compaction.process", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const msg = await user(session.id, "hello")
-        const msgs = await Session.messages({ sessionID: session.id })
+        const msgs = await svc.messages({ sessionID: session.id })
         const abort = new AbortController()
         const rt = liveRuntime(stub.layer, wide())
         let off: (() => void) | undefined
@@ -1063,9 +1084,9 @@ describe("session.compaction.process", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const msg = await user(session.id, "hello")
-        const msgs = await Session.messages({ sessionID: session.id })
+        const msgs = await svc.messages({ sessionID: session.id })
         const abort = new AbortController()
         const rt = runtime("continue", plugin(ready), wide())
         let run: Promise<"continue" | "stop"> | undefined
@@ -1100,7 +1121,7 @@ describe("session.compaction.process", () => {
           abort.abort()
           expect(await run).toBe("stop")
 
-          const all = await Session.messages({ sessionID: session.id })
+          const all = await svc.messages({ sessionID: session.id })
           expect(all.some((msg) => msg.info.role === "assistant" && msg.info.summary)).toBe(false)
         } finally {
           abort.abort()
@@ -1165,11 +1186,11 @@ describe("session.compaction.process", () => {
     await Instance.provide({
       directory: tmp.path,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const msg = await user(session.id, "hello")
         const rt = liveRuntime(stub.layer, wide())
         try {
-          const msgs = await Session.messages({ sessionID: session.id })
+          const msgs = await svc.messages({ sessionID: session.id })
           await rt.runPromise(
             SessionCompaction.Service.use((svc) =>
               svc.process({
@@ -1181,7 +1202,7 @@ describe("session.compaction.process", () => {
             ),
           )
 
-          const summary = (await Session.messages({ sessionID: session.id })).find(
+          const summary = (await svc.messages({ sessionID: session.id })).find(
             (item) => item.info.role === "assistant" && item.info.summary,
           )
 
@@ -1211,10 +1232,10 @@ describe("util.token.estimate", () => {
   })
 })
 
-describe("session.getUsage", () => {
+describe("SessionNs.getUsage", () => {
   test("normalizes standard usage to token format", () => {
     const model = createModel({ context: 100_000, output: 32_000 })
-    const result = Session.getUsage({
+    const result = SessionNs.getUsage({
       model,
       usage: {
         inputTokens: 1000,
@@ -1241,7 +1262,7 @@ describe("session.getUsage", () => {
 
   test("extracts cached tokens to cache.read", () => {
     const model = createModel({ context: 100_000, output: 32_000 })
-    const result = Session.getUsage({
+    const result = SessionNs.getUsage({
       model,
       usage: {
         inputTokens: 1000,
@@ -1265,7 +1286,7 @@ describe("session.getUsage", () => {
 
   test("handles anthropic cache write metadata", () => {
     const model = createModel({ context: 100_000, output: 32_000 })
-    const result = Session.getUsage({
+    const result = SessionNs.getUsage({
       model,
       usage: {
         inputTokens: 1000,
@@ -1294,7 +1315,7 @@ describe("session.getUsage", () => {
   test("subtracts cached tokens for anthropic provider", () => {
     const model = createModel({ context: 100_000, output: 32_000 })
     // AI SDK v6 normalizes inputTokens to include cached tokens for all providers
-    const result = Session.getUsage({
+    const result = SessionNs.getUsage({
       model,
       usage: {
         inputTokens: 1000,
@@ -1321,7 +1342,7 @@ describe("session.getUsage", () => {
 
   test("separates reasoning tokens from output tokens", () => {
     const model = createModel({ context: 100_000, output: 32_000 })
-    const result = Session.getUsage({
+    const result = SessionNs.getUsage({
       model,
       usage: {
         inputTokens: 1000,
@@ -1355,7 +1376,7 @@ describe("session.getUsage", () => {
         cache: { read: 0, write: 0 },
       },
     })
-    const result = Session.getUsage({
+    const result = SessionNs.getUsage({
       model,
       usage: {
         inputTokens: 0,
@@ -1380,7 +1401,7 @@ describe("session.getUsage", () => {
 
   test("handles undefined optional values gracefully", () => {
     const model = createModel({ context: 100_000, output: 32_000 })
-    const result = Session.getUsage({
+    const result = SessionNs.getUsage({
       model,
       usage: {
         inputTokens: 0,
@@ -1416,7 +1437,7 @@ describe("session.getUsage", () => {
         cache: { read: 0.3, write: 3.75 },
       },
     })
-    const result = Session.getUsage({
+    const result = SessionNs.getUsage({
       model,
       usage: {
         inputTokens: 1_000_000,
@@ -1457,7 +1478,7 @@ describe("session.getUsage", () => {
         },
       }
       if (npm === "@ai-sdk/amazon-bedrock") {
-        const result = Session.getUsage({
+        const result = SessionNs.getUsage({
           model,
           usage,
           metadata: {
@@ -1478,7 +1499,7 @@ describe("session.getUsage", () => {
         return
       }
 
-      const result = Session.getUsage({
+      const result = SessionNs.getUsage({
         model,
         usage,
         metadata: {
@@ -1499,7 +1520,7 @@ describe("session.getUsage", () => {
 
   test("extracts cache write tokens from vertex metadata key", () => {
     const model = createModel({ context: 100_000, output: 32_000, npm: "@ai-sdk/google-vertex/anthropic" })
-    const result = Session.getUsage({
+    const result = SessionNs.getUsage({
       model,
       usage: {
         inputTokens: 1000,

+ 112 - 91
packages/opencode/test/session/messages-pagination.test.ts

@@ -1,7 +1,8 @@
 import { describe, expect, test } from "bun:test"
+import { Effect } from "effect"
 import path from "path"
 import { Instance } from "../../src/project/instance"
-import { Session } from "../../src/session"
+import { Session as SessionNs } from "../../src/session"
 import { MessageV2 } from "../../src/session/message-v2"
 import { MessageID, PartID, type SessionID } from "../../src/session/schema"
 import { ModelID, ProviderID } from "../../src/provider/schema"
@@ -10,12 +11,32 @@ import { Log } from "../../src/util/log"
 const root = path.join(__dirname, "../..")
 Log.init({ print: false })
 
+function run<A, E>(fx: Effect.Effect<A, E, SessionNs.Service>) {
+  return Effect.runPromise(fx.pipe(Effect.provide(SessionNs.defaultLayer)))
+}
+
+const svc = {
+  ...SessionNs,
+  create(input?: SessionNs.CreateInput) {
+    return run(SessionNs.Service.use((svc) => svc.create(input)))
+  },
+  remove(id: SessionID) {
+    return run(SessionNs.Service.use((svc) => svc.remove(id)))
+  },
+  updateMessage<T extends MessageV2.Info>(msg: T) {
+    return run(SessionNs.Service.use((svc) => svc.updateMessage(msg)))
+  },
+  updatePart<T extends MessageV2.Part>(part: T) {
+    return run(SessionNs.Service.use((svc) => svc.updatePart(part)))
+  },
+}
+
 async function fill(sessionID: SessionID, count: number, time = (i: number) => Date.now() + i) {
   const ids = [] as MessageID[]
   for (let i = 0; i < count; i++) {
     const id = MessageID.ascending()
     ids.push(id)
-    await Session.updateMessage({
+    await svc.updateMessage({
       id,
       sessionID,
       role: "user",
@@ -25,7 +46,7 @@ async function fill(sessionID: SessionID, count: number, time = (i: number) => D
       tools: {},
       mode: "",
     } as unknown as MessageV2.Info)
-    await Session.updatePart({
+    await svc.updatePart({
       id: PartID.ascending(),
       sessionID,
       messageID: id,
@@ -38,7 +59,7 @@ async function fill(sessionID: SessionID, count: number, time = (i: number) => D
 
 async function addUser(sessionID: SessionID, text?: string) {
   const id = MessageID.ascending()
-  await Session.updateMessage({
+  await svc.updateMessage({
     id,
     sessionID,
     role: "user",
@@ -49,7 +70,7 @@ async function addUser(sessionID: SessionID, text?: string) {
     mode: "",
   } as unknown as MessageV2.Info)
   if (text) {
-    await Session.updatePart({
+    await svc.updatePart({
       id: PartID.ascending(),
       sessionID,
       messageID: id,
@@ -66,7 +87,7 @@ async function addAssistant(
   opts?: { summary?: boolean; finish?: string; error?: MessageV2.Assistant["error"] },
 ) {
   const id = MessageID.ascending()
-  await Session.updateMessage({
+  await svc.updateMessage({
     id,
     sessionID,
     role: "assistant",
@@ -87,7 +108,7 @@ async function addAssistant(
 }
 
 async function addCompactionPart(sessionID: SessionID, messageID: MessageID) {
-  await Session.updatePart({
+  await svc.updatePart({
     id: PartID.ascending(),
     sessionID,
     messageID,
@@ -101,14 +122,14 @@ describe("MessageV2.page", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         await fill(session.id, 2)
 
         const result = MessageV2.page({ sessionID: session.id, limit: 10 })
         expect(result).toBeDefined()
         expect(result.items).toBeArray()
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -117,7 +138,7 @@ describe("MessageV2.page", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const ids = await fill(session.id, 6)
 
         const a = MessageV2.page({ sessionID: session.id, limit: 2 })
@@ -136,7 +157,7 @@ describe("MessageV2.page", () => {
         expect(c.more).toBe(false)
         expect(c.cursor).toBeUndefined()
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -145,13 +166,13 @@ describe("MessageV2.page", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const ids = await fill(session.id, 4)
 
         const result = MessageV2.page({ sessionID: session.id, limit: 4 })
         expect(result.items.map((item) => item.info.id)).toEqual(ids)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -160,14 +181,14 @@ describe("MessageV2.page", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
 
         const result = MessageV2.page({ sessionID: session.id, limit: 10 })
         expect(result.items).toEqual([])
         expect(result.more).toBe(false)
         expect(result.cursor).toBeUndefined()
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -186,7 +207,7 @@ describe("MessageV2.page", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const ids = await fill(session.id, 3)
 
         const result = MessageV2.page({ sessionID: session.id, limit: 3 })
@@ -194,7 +215,7 @@ describe("MessageV2.page", () => {
         expect(result.more).toBe(false)
         expect(result.cursor).toBeUndefined()
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -203,7 +224,7 @@ describe("MessageV2.page", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const ids = await fill(session.id, 5)
 
         const result = MessageV2.page({ sessionID: session.id, limit: 1 })
@@ -211,7 +232,7 @@ describe("MessageV2.page", () => {
         expect(result.items[0].info.id).toBe(ids[ids.length - 1])
         expect(result.more).toBe(true)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -220,10 +241,10 @@ describe("MessageV2.page", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const [id] = await fill(session.id, 1)
 
-        await Session.updatePart({
+        await svc.updatePart({
           id: PartID.ascending(),
           sessionID: session.id,
           messageID: id,
@@ -235,7 +256,7 @@ describe("MessageV2.page", () => {
         expect(result.items).toHaveLength(1)
         expect(result.items[0].parts).toHaveLength(2)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -244,7 +265,7 @@ describe("MessageV2.page", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const ids = await fill(session.id, 4, (i) => 1000.5 + i)
 
         const a = MessageV2.page({ sessionID: session.id, limit: 2 })
@@ -253,7 +274,7 @@ describe("MessageV2.page", () => {
         expect(a.items.map((item) => item.info.id)).toEqual(ids.slice(-2))
         expect(b.items.map((item) => item.info.id)).toEqual(ids.slice(0, 2))
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -262,7 +283,7 @@ describe("MessageV2.page", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const ids = await fill(session.id, 4, () => 1000)
 
         const a = MessageV2.page({ sessionID: session.id, limit: 2 })
@@ -273,7 +294,7 @@ describe("MessageV2.page", () => {
         expect(b.items.map((item) => item.info.id)).toEqual(ids.slice(0, 2))
         expect(b.more).toBe(false)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -282,8 +303,8 @@ describe("MessageV2.page", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const a = await Session.create({})
-        const b = await Session.create({})
+        const a = await svc.create({})
+        const b = await svc.create({})
         await fill(a.id, 3)
         await fill(b.id, 2)
 
@@ -294,8 +315,8 @@ describe("MessageV2.page", () => {
         expect(resultA.items.every((item) => item.info.sessionID === a.id)).toBe(true)
         expect(resultB.items.every((item) => item.info.sessionID === b.id)).toBe(true)
 
-        await Session.remove(a.id)
-        await Session.remove(b.id)
+        await svc.remove(a.id)
+        await svc.remove(b.id)
       },
     })
   })
@@ -304,7 +325,7 @@ describe("MessageV2.page", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const ids = await fill(session.id, 10)
 
         const result = MessageV2.page({ sessionID: session.id, limit: 100 })
@@ -313,7 +334,7 @@ describe("MessageV2.page", () => {
         expect(result.more).toBe(false)
         expect(result.cursor).toBeUndefined()
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -324,13 +345,13 @@ describe("MessageV2.stream", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const ids = await fill(session.id, 5)
 
         const items = Array.from(MessageV2.stream(session.id))
         expect(items.map((item) => item.info.id)).toEqual(ids.slice().reverse())
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -339,12 +360,12 @@ describe("MessageV2.stream", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
 
         const items = Array.from(MessageV2.stream(session.id))
         expect(items).toHaveLength(0)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -353,14 +374,14 @@ describe("MessageV2.stream", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const ids = await fill(session.id, 1)
 
         const items = Array.from(MessageV2.stream(session.id))
         expect(items).toHaveLength(1)
         expect(items[0].info.id).toBe(ids[0])
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -369,7 +390,7 @@ describe("MessageV2.stream", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         await fill(session.id, 3)
 
         const items = Array.from(MessageV2.stream(session.id))
@@ -378,7 +399,7 @@ describe("MessageV2.stream", () => {
           expect(item.parts[0].type).toBe("text")
         }
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -387,7 +408,7 @@ describe("MessageV2.stream", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const ids = await fill(session.id, 60)
 
         const items = Array.from(MessageV2.stream(session.id))
@@ -395,7 +416,7 @@ describe("MessageV2.stream", () => {
         expect(items[0].info.id).toBe(ids[ids.length - 1])
         expect(items[59].info.id).toBe(ids[0])
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -404,7 +425,7 @@ describe("MessageV2.stream", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         await fill(session.id, 1)
 
         const gen = MessageV2.stream(session.id)
@@ -414,7 +435,7 @@ describe("MessageV2.stream", () => {
         expect(first).toHaveProperty("done")
         expect(first.done).toBe(false)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -425,7 +446,7 @@ describe("MessageV2.parts", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const [id] = await fill(session.id, 1)
 
         const result = MessageV2.parts(id)
@@ -433,7 +454,7 @@ describe("MessageV2.parts", () => {
         expect(result[0].type).toBe("text")
         expect((result[0] as MessageV2.TextPart).text).toBe("m0")
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -442,13 +463,13 @@ describe("MessageV2.parts", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const id = await addUser(session.id)
 
         const result = MessageV2.parts(id)
         expect(result).toEqual([])
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -457,17 +478,17 @@ describe("MessageV2.parts", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const [id] = await fill(session.id, 1)
 
-        await Session.updatePart({
+        await svc.updatePart({
           id: PartID.ascending(),
           sessionID: session.id,
           messageID: id,
           type: "text",
           text: "second",
         })
-        await Session.updatePart({
+        await svc.updatePart({
           id: PartID.ascending(),
           sessionID: session.id,
           messageID: id,
@@ -481,7 +502,7 @@ describe("MessageV2.parts", () => {
         expect((result[1] as MessageV2.TextPart).text).toBe("second")
         expect((result[2] as MessageV2.TextPart).text).toBe("third")
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -490,7 +511,7 @@ describe("MessageV2.parts", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        await Session.create({})
+        await svc.create({})
         const result = MessageV2.parts(MessageID.ascending())
         expect(result).toEqual([])
       },
@@ -501,14 +522,14 @@ describe("MessageV2.parts", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const [id] = await fill(session.id, 1)
 
         const result = MessageV2.parts(id)
         expect(result[0].sessionID).toBe(session.id)
         expect(result[0].messageID).toBe(id)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -519,7 +540,7 @@ describe("MessageV2.get", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const [id] = await fill(session.id, 1)
 
         const result = MessageV2.get({ sessionID: session.id, messageID: id })
@@ -529,7 +550,7 @@ describe("MessageV2.get", () => {
         expect(result.parts).toHaveLength(1)
         expect((result.parts[0] as MessageV2.TextPart).text).toBe("m0")
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -538,13 +559,13 @@ describe("MessageV2.get", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
 
         expect(() => MessageV2.get({ sessionID: session.id, messageID: MessageID.ascending() })).toThrow(
           "NotFoundError",
         )
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -553,16 +574,16 @@ describe("MessageV2.get", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const a = await Session.create({})
-        const b = await Session.create({})
+        const a = await svc.create({})
+        const b = await svc.create({})
         const [id] = await fill(a.id, 1)
 
         expect(() => MessageV2.get({ sessionID: b.id, messageID: id })).toThrow("NotFoundError")
         const result = MessageV2.get({ sessionID: a.id, messageID: id })
         expect(result.info.id).toBe(id)
 
-        await Session.remove(a.id)
-        await Session.remove(b.id)
+        await svc.remove(a.id)
+        await svc.remove(b.id)
       },
     })
   })
@@ -571,10 +592,10 @@ describe("MessageV2.get", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const [id] = await fill(session.id, 1)
 
-        await Session.updatePart({
+        await svc.updatePart({
           id: PartID.ascending(),
           sessionID: session.id,
           messageID: id,
@@ -585,7 +606,7 @@ describe("MessageV2.get", () => {
         const result = MessageV2.get({ sessionID: session.id, messageID: id })
         expect(result.parts).toHaveLength(2)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -594,11 +615,11 @@ describe("MessageV2.get", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const uid = await addUser(session.id, "hello")
         const aid = await addAssistant(session.id, uid)
 
-        await Session.updatePart({
+        await svc.updatePart({
           id: PartID.ascending(),
           sessionID: session.id,
           messageID: aid,
@@ -611,7 +632,7 @@ describe("MessageV2.get", () => {
         expect(result.parts).toHaveLength(1)
         expect((result.parts[0] as MessageV2.TextPart).text).toBe("response")
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -620,14 +641,14 @@ describe("MessageV2.get", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const id = await addUser(session.id)
 
         const result = MessageV2.get({ sessionID: session.id, messageID: id })
         expect(result.info.id).toBe(id)
         expect(result.parts).toEqual([])
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -638,7 +659,7 @@ describe("MessageV2.filterCompacted", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const ids = await fill(session.id, 5)
 
         const result = MessageV2.filterCompacted(MessageV2.stream(session.id))
@@ -646,7 +667,7 @@ describe("MessageV2.filterCompacted", () => {
         // reversed from newest-first to chronological
         expect(result.map((item) => item.info.id)).toEqual(ids)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -655,13 +676,13 @@ describe("MessageV2.filterCompacted", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
 
         // Chronological: u1(+compaction part), a1(summary, parentID=u1), u2, a2
         // Stream (newest first): a2, u2, a1(adds u1 to completed), u1(in completed + compaction) -> break
         const u1 = await addUser(session.id, "first question")
         const a1 = await addAssistant(session.id, u1, { summary: true, finish: "end_turn" })
-        await Session.updatePart({
+        await svc.updatePart({
           id: PartID.ascending(),
           sessionID: session.id,
           messageID: a1,
@@ -672,7 +693,7 @@ describe("MessageV2.filterCompacted", () => {
 
         const u2 = await addUser(session.id, "new question")
         const a2 = await addAssistant(session.id, u2)
-        await Session.updatePart({
+        await svc.updatePart({
           id: PartID.ascending(),
           sessionID: session.id,
           messageID: a2,
@@ -685,7 +706,7 @@ describe("MessageV2.filterCompacted", () => {
         expect(result[0].info.id).toBe(u1)
         expect(result.length).toBe(4)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -699,7 +720,7 @@ describe("MessageV2.filterCompacted", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
 
         const u1 = await addUser(session.id, "hello")
         await addCompactionPart(session.id, u1)
@@ -708,7 +729,7 @@ describe("MessageV2.filterCompacted", () => {
         const result = MessageV2.filterCompacted(MessageV2.stream(session.id))
         expect(result).toHaveLength(2)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -717,7 +738,7 @@ describe("MessageV2.filterCompacted", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
 
         const u1 = await addUser(session.id, "hello")
         await addCompactionPart(session.id, u1)
@@ -733,7 +754,7 @@ describe("MessageV2.filterCompacted", () => {
         // Error assistant doesn't add to completed, so compaction boundary never triggers
         expect(result).toHaveLength(3)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -742,7 +763,7 @@ describe("MessageV2.filterCompacted", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
 
         const u1 = await addUser(session.id, "hello")
         await addCompactionPart(session.id, u1)
@@ -754,7 +775,7 @@ describe("MessageV2.filterCompacted", () => {
         const result = MessageV2.filterCompacted(MessageV2.stream(session.id))
         expect(result).toHaveLength(3)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -808,7 +829,7 @@ describe("MessageV2 consistency", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         await fill(session.id, 3)
 
         const paged = MessageV2.page({ sessionID: session.id, limit: 10 })
@@ -818,7 +839,7 @@ describe("MessageV2 consistency", () => {
           expect(got.parts).toEqual(item.parts)
         }
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -827,14 +848,14 @@ describe("MessageV2 consistency", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const [id] = await fill(session.id, 1)
 
         const got = MessageV2.get({ sessionID: session.id, messageID: id })
         const standalone = MessageV2.parts(id)
         expect(got.parts).toEqual(standalone)
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -843,7 +864,7 @@ describe("MessageV2 consistency", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         await fill(session.id, 7)
 
         const streamed = Array.from(MessageV2.stream(session.id))
@@ -861,7 +882,7 @@ describe("MessageV2 consistency", () => {
 
         expect(streamed.map((m) => m.info.id)).toEqual(paged.map((m) => m.info.id))
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })
@@ -870,7 +891,7 @@ describe("MessageV2 consistency", () => {
     await Instance.provide({
       directory: root,
       fn: async () => {
-        const session = await Session.create({})
+        const session = await svc.create({})
         const ids = await fill(session.id, 4)
 
         const filtered = MessageV2.filterCompacted(MessageV2.stream(session.id))
@@ -878,7 +899,7 @@ describe("MessageV2 consistency", () => {
 
         expect(filtered.map((m) => m.info.id)).toEqual(all.map((m) => m.info.id))
 
-        await Session.remove(session.id)
+        await svc.remove(session.id)
       },
     })
   })

+ 34 - 40
packages/opencode/test/session/prompt-effect.test.ts

@@ -210,7 +210,7 @@ function makeHttp() {
       Layer.provide(SystemPrompt.defaultLayer),
       Layer.provideMerge(deps),
     ),
-  )
+  ).pipe(Layer.provide(summary))
 }
 
 const it = testEffect(makeHttp())
@@ -384,25 +384,23 @@ it.live("loop calls LLM and returns assistant message", () =>
 it.live("static loop returns assistant text through local provider", () =>
   provideTmpdirServer(
     Effect.fnUntraced(function* ({ llm }) {
-      const session = yield* Effect.promise(() =>
-        Session.create({
-          title: "Prompt provider",
-          permission: [{ permission: "*", pattern: "*", action: "allow" }],
-        }),
-      )
+      const prompt = yield* SessionPrompt.Service
+      const sessions = yield* Session.Service
+      const session = yield* sessions.create({
+        title: "Prompt provider",
+        permission: [{ permission: "*", pattern: "*", action: "allow" }],
+      })
 
-      yield* Effect.promise(() =>
-        SessionPrompt.prompt({
-          sessionID: session.id,
-          agent: "build",
-          noReply: true,
-          parts: [{ type: "text", text: "hello" }],
-        }),
-      )
+      yield* prompt.prompt({
+        sessionID: session.id,
+        agent: "build",
+        noReply: true,
+        parts: [{ type: "text", text: "hello" }],
+      })
 
       yield* llm.text("world")
 
-      const result = yield* Effect.promise(() => SessionPrompt.loop({ sessionID: session.id }))
+      const result = yield* prompt.loop({ sessionID: session.id })
       expect(result.info.role).toBe("assistant")
       expect(result.parts.some((part) => part.type === "text" && part.text === "world")).toBe(true)
       expect(yield* llm.hits).toHaveLength(1)
@@ -415,40 +413,36 @@ it.live("static loop returns assistant text through local provider", () =>
 it.live("static loop consumes queued replies across turns", () =>
   provideTmpdirServer(
     Effect.fnUntraced(function* ({ llm }) {
-      const session = yield* Effect.promise(() =>
-        Session.create({
-          title: "Prompt provider turns",
-          permission: [{ permission: "*", pattern: "*", action: "allow" }],
-        }),
-      )
+      const prompt = yield* SessionPrompt.Service
+      const sessions = yield* Session.Service
+      const session = yield* sessions.create({
+        title: "Prompt provider turns",
+        permission: [{ permission: "*", pattern: "*", action: "allow" }],
+      })
 
-      yield* Effect.promise(() =>
-        SessionPrompt.prompt({
-          sessionID: session.id,
-          agent: "build",
-          noReply: true,
-          parts: [{ type: "text", text: "hello one" }],
-        }),
-      )
+      yield* prompt.prompt({
+        sessionID: session.id,
+        agent: "build",
+        noReply: true,
+        parts: [{ type: "text", text: "hello one" }],
+      })
 
       yield* llm.text("world one")
 
-      const first = yield* Effect.promise(() => SessionPrompt.loop({ sessionID: session.id }))
+      const first = yield* prompt.loop({ sessionID: session.id })
       expect(first.info.role).toBe("assistant")
       expect(first.parts.some((part) => part.type === "text" && part.text === "world one")).toBe(true)
 
-      yield* Effect.promise(() =>
-        SessionPrompt.prompt({
-          sessionID: session.id,
-          agent: "build",
-          noReply: true,
-          parts: [{ type: "text", text: "hello two" }],
-        }),
-      )
+      yield* prompt.prompt({
+        sessionID: session.id,
+        agent: "build",
+        noReply: true,
+        parts: [{ type: "text", text: "hello two" }],
+      })
 
       yield* llm.text("world two")
 
-      const second = yield* Effect.promise(() => SessionPrompt.loop({ sessionID: session.id }))
+      const second = yield* prompt.loop({ sessionID: session.id })
       expect(second.info.role).toBe("assistant")
       expect(second.parts.some((part) => part.type === "text" && part.text === "world two")).toBe(true)
 

+ 283 - 215
packages/opencode/test/session/prompt.test.ts

@@ -2,6 +2,7 @@ import path from "path"
 import { describe, expect, test } from "bun:test"
 import { NamedError } from "@opencode-ai/util/error"
 import { fileURLToPath } from "url"
+import { Effect, Layer } from "effect"
 import { Instance } from "../../src/project/instance"
 import { ModelID, ProviderID } from "../../src/provider/schema"
 import { Session } from "../../src/session"
@@ -12,6 +13,12 @@ import { tmpdir } from "../fixture/fixture"
 
 Log.init({ print: false })
 
+function run<A, E>(fx: Effect.Effect<A, E, SessionPrompt.Service | Session.Service>) {
+  return Effect.runPromise(
+    fx.pipe(Effect.scoped, Effect.provide(Layer.mergeAll(SessionPrompt.defaultLayer, Session.defaultLayer))),
+  )
+}
+
 function defer<T>() {
   let resolve!: (value: T | PromiseLike<T>) => void
   const promise = new Promise<T>((done) => {
@@ -104,34 +111,39 @@ describe("session.prompt missing file", () => {
 
     await Instance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const session = await Session.create({})
-
-        const missing = path.join(tmp.path, "does-not-exist.ts")
-        const msg = await SessionPrompt.prompt({
-          sessionID: session.id,
-          agent: "build",
-          noReply: true,
-          parts: [
-            { type: "text", text: "please review @does-not-exist.ts" },
-            {
-              type: "file",
-              mime: "text/plain",
-              url: `file://${missing}`,
-              filename: "does-not-exist.ts",
-            },
-          ],
-        })
+      fn: () =>
+        run(
+          Effect.gen(function* () {
+            const prompt = yield* SessionPrompt.Service
+            const sessions = yield* Session.Service
+            const session = yield* sessions.create({})
+
+            const missing = path.join(tmp.path, "does-not-exist.ts")
+            const msg = yield* prompt.prompt({
+              sessionID: session.id,
+              agent: "build",
+              noReply: true,
+              parts: [
+                { type: "text", text: "please review @does-not-exist.ts" },
+                {
+                  type: "file",
+                  mime: "text/plain",
+                  url: `file://${missing}`,
+                  filename: "does-not-exist.ts",
+                },
+              ],
+            })
 
-        if (msg.info.role !== "user") throw new Error("expected user message")
+            if (msg.info.role !== "user") throw new Error("expected user message")
 
-        const hasFailure = msg.parts.some(
-          (part) => part.type === "text" && part.synthetic && part.text.includes("Read tool failed to read"),
-        )
-        expect(hasFailure).toBe(true)
+            const hasFailure = msg.parts.some(
+              (part) => part.type === "text" && part.synthetic && part.text.includes("Read tool failed to read"),
+            )
+            expect(hasFailure).toBe(true)
 
-        await Session.remove(session.id)
-      },
+            yield* sessions.remove(session.id)
+          }),
+        ),
     })
   })
 
@@ -149,39 +161,44 @@ describe("session.prompt missing file", () => {
 
     await Instance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const session = await Session.create({})
-
-        const missing = path.join(tmp.path, "still-missing.ts")
-        const msg = await SessionPrompt.prompt({
-          sessionID: session.id,
-          agent: "build",
-          noReply: true,
-          parts: [
-            {
-              type: "file",
-              mime: "text/plain",
-              url: `file://${missing}`,
-              filename: "still-missing.ts",
-            },
-            { type: "text", text: "after-file" },
-          ],
-        })
+      fn: () =>
+        run(
+          Effect.gen(function* () {
+            const prompt = yield* SessionPrompt.Service
+            const sessions = yield* Session.Service
+            const session = yield* sessions.create({})
+
+            const missing = path.join(tmp.path, "still-missing.ts")
+            const msg = yield* prompt.prompt({
+              sessionID: session.id,
+              agent: "build",
+              noReply: true,
+              parts: [
+                {
+                  type: "file",
+                  mime: "text/plain",
+                  url: `file://${missing}`,
+                  filename: "still-missing.ts",
+                },
+                { type: "text", text: "after-file" },
+              ],
+            })
 
-        if (msg.info.role !== "user") throw new Error("expected user message")
+            if (msg.info.role !== "user") throw new Error("expected user message")
 
-        const stored = await MessageV2.get({
-          sessionID: session.id,
-          messageID: msg.info.id,
-        })
-        const text = stored.parts.filter((part) => part.type === "text").map((part) => part.text)
+            const stored = MessageV2.get({
+              sessionID: session.id,
+              messageID: msg.info.id,
+            })
+            const text = stored.parts.filter((part) => part.type === "text").map((part) => part.text)
 
-        expect(text[0]?.startsWith("Called the Read tool with the following input:")).toBe(true)
-        expect(text[1]?.includes("Read tool failed to read")).toBe(true)
-        expect(text[2]).toBe("after-file")
+            expect(text[0]?.startsWith("Called the Read tool with the following input:")).toBe(true)
+            expect(text[1]?.includes("Read tool failed to read")).toBe(true)
+            expect(text[2]).toBe("after-file")
 
-        await Session.remove(session.id)
-      },
+            yield* sessions.remove(session.id)
+          }),
+        ),
     })
   })
 })
@@ -197,31 +214,36 @@ describe("session.prompt special characters", () => {
 
     await Instance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const session = await Session.create({})
-        const template = "Read @file#name.txt"
-        const parts = await SessionPrompt.resolvePromptParts(template)
-        const fileParts = parts.filter((part) => part.type === "file")
-
-        expect(fileParts.length).toBe(1)
-        expect(fileParts[0].filename).toBe("file#name.txt")
-        expect(fileParts[0].url).toContain("%23")
-
-        const decodedPath = fileURLToPath(fileParts[0].url)
-        expect(decodedPath).toBe(path.join(tmp.path, "file#name.txt"))
-
-        const message = await SessionPrompt.prompt({
-          sessionID: session.id,
-          parts,
-          noReply: true,
-        })
-        const stored = await MessageV2.get({ sessionID: session.id, messageID: message.info.id })
-        const textParts = stored.parts.filter((part) => part.type === "text")
-        const hasContent = textParts.some((part) => part.text.includes("special content"))
-        expect(hasContent).toBe(true)
-
-        await Session.remove(session.id)
-      },
+      fn: () =>
+        run(
+          Effect.gen(function* () {
+            const prompt = yield* SessionPrompt.Service
+            const sessions = yield* Session.Service
+            const session = yield* sessions.create({})
+            const template = "Read @file#name.txt"
+            const parts = yield* prompt.resolvePromptParts(template)
+            const fileParts = parts.filter((part) => part.type === "file")
+
+            expect(fileParts.length).toBe(1)
+            expect(fileParts[0].filename).toBe("file#name.txt")
+            expect(fileParts[0].url).toContain("%23")
+
+            const decodedPath = fileURLToPath(fileParts[0].url)
+            expect(decodedPath).toBe(path.join(tmp.path, "file#name.txt"))
+
+            const message = yield* prompt.prompt({
+              sessionID: session.id,
+              parts,
+              noReply: true,
+            })
+            const stored = MessageV2.get({ sessionID: session.id, messageID: message.info.id })
+            const textParts = stored.parts.filter((part) => part.type === "text")
+            const hasContent = textParts.some((part) => part.text.includes("special content"))
+            expect(hasContent).toBe(true)
+
+            yield* sessions.remove(session.id)
+          }),
+        ),
     })
   })
 })
@@ -273,21 +295,26 @@ describe("session.prompt regression", () => {
 
       await Instance.provide({
         directory: tmp.path,
-        fn: async () => {
-          const session = await Session.create({ title: "Prompt regression" })
-          const result = await SessionPrompt.prompt({
-            sessionID: session.id,
-            agent: "build",
-            parts: [{ type: "text", text: "Where is SessionProcessor?" }],
-          })
-
-          expect(result.info.role).toBe("assistant")
-          expect(result.parts.some((part) => part.type === "text" && part.text.includes("processor.ts"))).toBe(true)
-
-          const msgs = await Session.messages({ sessionID: session.id })
-          expect(msgs.filter((msg) => msg.info.role === "assistant")).toHaveLength(1)
-          expect(calls).toBe(1)
-        },
+        fn: () =>
+          run(
+            Effect.gen(function* () {
+              const prompt = yield* SessionPrompt.Service
+              const sessions = yield* Session.Service
+              const session = yield* sessions.create({ title: "Prompt regression" })
+              const result = yield* prompt.prompt({
+                sessionID: session.id,
+                agent: "build",
+                parts: [{ type: "text", text: "Where is SessionProcessor?" }],
+              })
+
+              expect(result.info.role).toBe("assistant")
+              expect(result.parts.some((part) => part.type === "text" && part.text.includes("processor.ts"))).toBe(true)
+
+              const msgs = yield* sessions.messages({ sessionID: session.id })
+              expect(msgs.filter((msg) => msg.info.role === "assistant")).toHaveLength(1)
+              expect(calls).toBe(1)
+            }),
+          ),
       })
     } finally {
       server.stop(true)
@@ -342,36 +369,45 @@ describe("session.prompt regression", () => {
 
       await Instance.provide({
         directory: tmp.path,
-        fn: async () => {
-          const session = await Session.create({ title: "Prompt cancel regression" })
-          const run = SessionPrompt.prompt({
-            sessionID: session.id,
-            agent: "build",
-            parts: [{ type: "text", text: "Cancel me" }],
-          })
-
-          await ready.promise
-          await SessionPrompt.cancel(session.id)
-
-          const result = await Promise.race([
-            run,
-            new Promise<never>((_, reject) =>
-              setTimeout(() => reject(new Error("timed out waiting for cancel")), 1000),
-            ),
-          ])
-
-          expect(result.info.role).toBe("assistant")
-          if (result.info.role === "assistant") {
-            expect(result.info.error?.name).toBe("MessageAbortedError")
-          }
-
-          const msgs = await Session.messages({ sessionID: session.id })
-          const last = msgs.findLast((msg) => msg.info.role === "assistant")
-          expect(last?.info.role).toBe("assistant")
-          if (last?.info.role === "assistant") {
-            expect(last.info.error?.name).toBe("MessageAbortedError")
-          }
-        },
+        fn: () =>
+          run(
+            Effect.gen(function* () {
+              const prompt = yield* SessionPrompt.Service
+              const sessions = yield* Session.Service
+              const session = yield* sessions.create({ title: "Prompt cancel regression" })
+              const task = Effect.runPromise(
+                prompt.prompt({
+                  sessionID: session.id,
+                  agent: "build",
+                  parts: [{ type: "text", text: "Cancel me" }],
+                }),
+              )
+
+              yield* Effect.promise(() => ready.promise)
+              yield* prompt.cancel(session.id)
+
+              const result = yield* Effect.promise(() =>
+                Promise.race([
+                  task,
+                  new Promise<never>((_, reject) =>
+                    setTimeout(() => reject(new Error("timed out waiting for cancel")), 1000),
+                  ),
+                ]),
+              )
+
+              expect(result.info.role).toBe("assistant")
+              if (result.info.role === "assistant") {
+                expect(result.info.error?.name).toBe("MessageAbortedError")
+              }
+
+              const msgs = yield* sessions.messages({ sessionID: session.id })
+              const last = msgs.findLast((msg) => msg.info.role === "assistant")
+              expect(last?.info.role).toBe("assistant")
+              if (last?.info.role === "assistant") {
+                expect(last.info.error?.name).toBe("MessageAbortedError")
+              }
+            }),
+          ),
       })
     } finally {
       server.stop(true)
@@ -399,45 +435,50 @@ describe("session.prompt agent variant", () => {
 
       await Instance.provide({
         directory: tmp.path,
-        fn: async () => {
-          const session = await Session.create({})
-
-          const other = await SessionPrompt.prompt({
-            sessionID: session.id,
-            agent: "build",
-            model: { providerID: ProviderID.make("opencode"), modelID: ModelID.make("kimi-k2.5-free") },
-            noReply: true,
-            parts: [{ type: "text", text: "hello" }],
-          })
-          if (other.info.role !== "user") throw new Error("expected user message")
-          expect(other.info.model.variant).toBeUndefined()
-
-          const match = await SessionPrompt.prompt({
-            sessionID: session.id,
-            agent: "build",
-            noReply: true,
-            parts: [{ type: "text", text: "hello again" }],
-          })
-          if (match.info.role !== "user") throw new Error("expected user message")
-          expect(match.info.model).toEqual({
-            providerID: ProviderID.make("openai"),
-            modelID: ModelID.make("gpt-5.2"),
-            variant: "xhigh",
-          })
-          expect(match.info.model.variant).toBe("xhigh")
-
-          const override = await SessionPrompt.prompt({
-            sessionID: session.id,
-            agent: "build",
-            noReply: true,
-            variant: "high",
-            parts: [{ type: "text", text: "hello third" }],
-          })
-          if (override.info.role !== "user") throw new Error("expected user message")
-          expect(override.info.model.variant).toBe("high")
-
-          await Session.remove(session.id)
-        },
+        fn: () =>
+          run(
+            Effect.gen(function* () {
+              const prompt = yield* SessionPrompt.Service
+              const sessions = yield* Session.Service
+              const session = yield* sessions.create({})
+
+              const other = yield* prompt.prompt({
+                sessionID: session.id,
+                agent: "build",
+                model: { providerID: ProviderID.make("opencode"), modelID: ModelID.make("kimi-k2.5-free") },
+                noReply: true,
+                parts: [{ type: "text", text: "hello" }],
+              })
+              if (other.info.role !== "user") throw new Error("expected user message")
+              expect(other.info.model.variant).toBeUndefined()
+
+              const match = yield* prompt.prompt({
+                sessionID: session.id,
+                agent: "build",
+                noReply: true,
+                parts: [{ type: "text", text: "hello again" }],
+              })
+              if (match.info.role !== "user") throw new Error("expected user message")
+              expect(match.info.model).toEqual({
+                providerID: ProviderID.make("openai"),
+                modelID: ModelID.make("gpt-5.2"),
+                variant: "xhigh",
+              })
+              expect(match.info.model.variant).toBe("xhigh")
+
+              const override = yield* prompt.prompt({
+                sessionID: session.id,
+                agent: "build",
+                noReply: true,
+                variant: "high",
+                parts: [{ type: "text", text: "hello third" }],
+              })
+              if (override.info.role !== "user") throw new Error("expected user message")
+              expect(override.info.model.variant).toBe("high")
+
+              yield* sessions.remove(session.id)
+            }),
+          ),
       })
     } finally {
       if (prev === undefined) delete process.env.OPENAI_API_KEY
@@ -451,24 +492,33 @@ describe("session.agent-resolution", () => {
     await using tmp = await tmpdir({ git: true })
     await Instance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const session = await Session.create({})
-        const err = await SessionPrompt.prompt({
-          sessionID: session.id,
-          agent: "nonexistent-agent-xyz",
-          noReply: true,
-          parts: [{ type: "text", text: "hello" }],
-        }).then(
-          () => undefined,
-          (e) => e,
-        )
-        expect(err).toBeDefined()
-        expect(err).not.toBeInstanceOf(TypeError)
-        expect(NamedError.Unknown.isInstance(err)).toBe(true)
-        if (NamedError.Unknown.isInstance(err)) {
-          expect(err.data.message).toContain('Agent not found: "nonexistent-agent-xyz"')
-        }
-      },
+      fn: () =>
+        run(
+          Effect.gen(function* () {
+            const prompt = yield* SessionPrompt.Service
+            const sessions = yield* Session.Service
+            const session = yield* sessions.create({})
+            const err = yield* Effect.promise(() =>
+              Effect.runPromise(
+                prompt.prompt({
+                  sessionID: session.id,
+                  agent: "nonexistent-agent-xyz",
+                  noReply: true,
+                  parts: [{ type: "text", text: "hello" }],
+                }),
+              ).then(
+                () => undefined,
+                (e) => e,
+              ),
+            )
+            expect(err).toBeDefined()
+            expect(err).not.toBeInstanceOf(TypeError)
+            expect(NamedError.Unknown.isInstance(err)).toBe(true)
+            if (NamedError.Unknown.isInstance(err)) {
+              expect(err.data.message).toContain('Agent not found: "nonexistent-agent-xyz"')
+            }
+          }),
+        ),
     })
   }, 30000)
 
@@ -476,22 +526,31 @@ describe("session.agent-resolution", () => {
     await using tmp = await tmpdir({ git: true })
     await Instance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const session = await Session.create({})
-        const err = await SessionPrompt.prompt({
-          sessionID: session.id,
-          agent: "nonexistent-agent-xyz",
-          noReply: true,
-          parts: [{ type: "text", text: "hello" }],
-        }).then(
-          () => undefined,
-          (e) => e,
-        )
-        expect(NamedError.Unknown.isInstance(err)).toBe(true)
-        if (NamedError.Unknown.isInstance(err)) {
-          expect(err.data.message).toContain("build")
-        }
-      },
+      fn: () =>
+        run(
+          Effect.gen(function* () {
+            const prompt = yield* SessionPrompt.Service
+            const sessions = yield* Session.Service
+            const session = yield* sessions.create({})
+            const err = yield* Effect.promise(() =>
+              Effect.runPromise(
+                prompt.prompt({
+                  sessionID: session.id,
+                  agent: "nonexistent-agent-xyz",
+                  noReply: true,
+                  parts: [{ type: "text", text: "hello" }],
+                }),
+              ).then(
+                () => undefined,
+                (e) => e,
+              ),
+            )
+            expect(NamedError.Unknown.isInstance(err)).toBe(true)
+            if (NamedError.Unknown.isInstance(err)) {
+              expect(err.data.message).toContain("build")
+            }
+          }),
+        ),
     })
   }, 30000)
 
@@ -499,24 +558,33 @@ describe("session.agent-resolution", () => {
     await using tmp = await tmpdir({ git: true })
     await Instance.provide({
       directory: tmp.path,
-      fn: async () => {
-        const session = await Session.create({})
-        const err = await SessionPrompt.command({
-          sessionID: session.id,
-          command: "nonexistent-command-xyz",
-          arguments: "",
-        }).then(
-          () => undefined,
-          (e) => e,
-        )
-        expect(err).toBeDefined()
-        expect(err).not.toBeInstanceOf(TypeError)
-        expect(NamedError.Unknown.isInstance(err)).toBe(true)
-        if (NamedError.Unknown.isInstance(err)) {
-          expect(err.data.message).toContain('Command not found: "nonexistent-command-xyz"')
-          expect(err.data.message).toContain("init")
-        }
-      },
+      fn: () =>
+        run(
+          Effect.gen(function* () {
+            const prompt = yield* SessionPrompt.Service
+            const sessions = yield* Session.Service
+            const session = yield* sessions.create({})
+            const err = yield* Effect.promise(() =>
+              Effect.runPromise(
+                prompt.command({
+                  sessionID: session.id,
+                  command: "nonexistent-command-xyz",
+                  arguments: "",
+                }),
+              ).then(
+                () => undefined,
+                (e) => e,
+              ),
+            )
+            expect(err).toBeDefined()
+            expect(err).not.toBeInstanceOf(TypeError)
+            expect(NamedError.Unknown.isInstance(err)).toBe(true)
+            if (NamedError.Unknown.isInstance(err)) {
+              expect(err.data.message).toContain('Command not found: "nonexistent-command-xyz"')
+              expect(err.data.message).toContain("init")
+            }
+          }),
+        ),
     })
   }, 30000)
 })

+ 46 - 30
packages/opencode/test/session/session.test.ts

@@ -1,43 +1,62 @@
 import { describe, expect, test } from "bun:test"
 import path from "path"
-import { Session } from "../../src/session"
+import { Session as SessionNs } from "../../src/session"
 import { Bus } from "../../src/bus"
 import { Log } from "../../src/util/log"
 import { Instance } from "../../src/project/instance"
 import { MessageV2 } from "../../src/session/message-v2"
-import { MessageID, PartID } from "../../src/session/schema"
+import { MessageID, PartID, type SessionID } from "../../src/session/schema"
+import { AppRuntime } from "../../src/effect/app-runtime"
 import { tmpdir } from "../fixture/fixture"
 
 const projectRoot = path.join(__dirname, "../..")
 Log.init({ print: false })
 
+function create(input?: SessionNs.CreateInput) {
+  return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create(input)))
+}
+
+function get(id: SessionID) {
+  return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.get(id)))
+}
+
+function remove(id: SessionID) {
+  return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.remove(id)))
+}
+
+function updateMessage<T extends MessageV2.Info>(msg: T) {
+  return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.updateMessage(msg)))
+}
+
+function updatePart<T extends MessageV2.Part>(part: T) {
+  return AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.updatePart(part)))
+}
+
 describe("session.created event", () => {
   test("should emit session.created event when session is created", async () => {
     await Instance.provide({
       directory: projectRoot,
       fn: async () => {
         let eventReceived = false
-        let receivedInfo: Session.Info | undefined
+        let receivedInfo: SessionNs.Info | undefined
 
-        const unsub = Bus.subscribe(Session.Event.Created, (event) => {
+        const unsub = Bus.subscribe(SessionNs.Event.Created, (event) => {
           eventReceived = true
-          receivedInfo = event.properties.info as Session.Info
+          receivedInfo = event.properties.info as SessionNs.Info
         })
 
-        const session = await Session.create({})
-
+        const info = await create({})
         await new Promise((resolve) => setTimeout(resolve, 100))
-
         unsub()
 
         expect(eventReceived).toBe(true)
         expect(receivedInfo).toBeDefined()
-        expect(receivedInfo?.id).toBe(session.id)
-        expect(receivedInfo?.projectID).toBe(session.projectID)
-        expect(receivedInfo?.directory).toBe(session.directory)
-        expect(receivedInfo?.title).toBe(session.title)
+        expect(receivedInfo?.id).toBe(info.id)
+        expect(receivedInfo?.projectID).toBe(info.projectID)
+        expect(receivedInfo?.directory).toBe(info.directory)
+        expect(receivedInfo?.title).toBe(info.title)
 
-        await Session.remove(session.id)
+        await remove(info.id)
       },
     })
   })
@@ -48,18 +67,16 @@ describe("session.created event", () => {
       fn: async () => {
         const events: string[] = []
 
-        const unsubCreated = Bus.subscribe(Session.Event.Created, () => {
+        const unsubCreated = Bus.subscribe(SessionNs.Event.Created, () => {
           events.push("created")
         })
 
-        const unsubUpdated = Bus.subscribe(Session.Event.Updated, () => {
+        const unsubUpdated = Bus.subscribe(SessionNs.Event.Updated, () => {
           events.push("updated")
         })
 
-        const session = await Session.create({})
-
+        const info = await create({})
         await new Promise((resolve) => setTimeout(resolve, 100))
-
         unsubCreated()
         unsubUpdated()
 
@@ -67,7 +84,7 @@ describe("session.created event", () => {
         expect(events).toContain("updated")
         expect(events.indexOf("created")).toBeLessThan(events.indexOf("updated"))
 
-        await Session.remove(session.id)
+        await remove(info.id)
       },
     })
   })
@@ -80,12 +97,12 @@ describe("step-finish token propagation via Bus event", () => {
       await Instance.provide({
         directory: projectRoot,
         fn: async () => {
-          const session = await Session.create({})
+          const info = await create({})
 
           const messageID = MessageID.ascending()
-          await Session.updateMessage({
+          await updateMessage({
             id: messageID,
-            sessionID: session.id,
+            sessionID: info.id,
             role: "user",
             time: { created: Date.now() },
             agent: "user",
@@ -110,15 +127,14 @@ describe("step-finish token propagation via Bus event", () => {
           const partInput = {
             id: PartID.ascending(),
             messageID,
-            sessionID: session.id,
+            sessionID: info.id,
             type: "step-finish" as const,
             reason: "stop",
             cost: 0.005,
             tokens,
           }
 
-          await Session.updatePart(partInput)
-
+          await updatePart(partInput)
           await new Promise((resolve) => setTimeout(resolve, 100))
 
           expect(received).toBeDefined()
@@ -134,7 +150,7 @@ describe("step-finish token propagation via Bus event", () => {
           expect(received).not.toBe(partInput)
 
           unsub()
-          await Session.remove(session.id)
+          await remove(info.id)
         },
       })
     },
@@ -146,17 +162,17 @@ describe("Session", () => {
   test("remove works without an instance", async () => {
     await using tmp = await tmpdir({ git: true })
 
-    const session = await Instance.provide({
+    const info = await Instance.provide({
       directory: tmp.path,
-      fn: async () => Session.create({ title: "remove-without-instance" }),
+      fn: () => create({ title: "remove-without-instance" }),
     })
 
     await expect(async () => {
-      await Session.remove(session.id)
+      await remove(info.id)
     }).not.toThrow()
 
     let missing = false
-    await Session.get(session.id).catch(() => {
+    await get(info.id).catch(() => {
       missing = true
     })
 

+ 191 - 160
packages/opencode/test/session/structured-output-integration.test.ts

@@ -1,5 +1,6 @@
 import { describe, expect, test } from "bun:test"
 import path from "path"
+import { Effect, Layer } from "effect"
 import { Session } from "../../src/session"
 import { SessionPrompt } from "../../src/session/prompt"
 import { Log } from "../../src/util/log"
@@ -20,51 +21,63 @@ async function withInstance<T>(fn: () => Promise<T>): Promise<T> {
   })
 }
 
+function run<A, E>(fx: Effect.Effect<A, E, SessionPrompt.Service | Session.Service>) {
+  return Effect.runPromise(
+    fx.pipe(Effect.scoped, Effect.provide(Layer.mergeAll(SessionPrompt.defaultLayer, Session.defaultLayer))),
+  )
+}
+
 describe("StructuredOutput Integration", () => {
   test.skipIf(!hasApiKey)(
     "produces structured output with simple schema",
     async () => {
-      await withInstance(async () => {
-        const session = await Session.create({ title: "Structured Output Test" })
-
-        const result = await SessionPrompt.prompt({
-          sessionID: session.id,
-          parts: [
-            {
-              type: "text",
-              text: "What is 2 + 2? Provide a simple answer.",
-            },
-          ],
-          format: {
-            type: "json_schema",
-            schema: {
-              type: "object",
-              properties: {
-                answer: { type: "number", description: "The numerical answer" },
-                explanation: { type: "string", description: "Brief explanation" },
+      await withInstance(() =>
+        run(
+          Effect.gen(function* () {
+            const prompt = yield* SessionPrompt.Service
+            const sessions = yield* Session.Service
+            const session = yield* sessions.create({ title: "Structured Output Test" })
+
+            const result = yield* prompt.prompt({
+              sessionID: session.id,
+              parts: [
+                {
+                  type: "text",
+                  text: "What is 2 + 2? Provide a simple answer.",
+                },
+              ],
+              format: {
+                type: "json_schema",
+                schema: {
+                  type: "object",
+                  properties: {
+                    answer: { type: "number", description: "The numerical answer" },
+                    explanation: { type: "string", description: "Brief explanation" },
+                  },
+                  required: ["answer"],
+                },
+                retryCount: 0,
               },
-              required: ["answer"],
-            },
-            retryCount: 0,
-          },
-        })
-
-        // Verify structured output was captured (only on assistant messages)
-        expect(result.info.role).toBe("assistant")
-        if (result.info.role === "assistant") {
-          expect(result.info.structured).toBeDefined()
-          expect(typeof result.info.structured).toBe("object")
-
-          const output = result.info.structured as any
-          expect(output.answer).toBe(4)
-
-          // Verify no error was set
-          expect(result.info.error).toBeUndefined()
-        }
-
-        // Clean up
-        // Note: Not removing session to avoid race with background SessionSummary.summarize
-      })
+            })
+
+            // Verify structured output was captured (only on assistant messages)
+            expect(result.info.role).toBe("assistant")
+            if (result.info.role === "assistant") {
+              expect(result.info.structured).toBeDefined()
+              expect(typeof result.info.structured).toBe("object")
+
+              const output = result.info.structured as any
+              expect(output.answer).toBe(4)
+
+              // Verify no error was set
+              expect(result.info.error).toBeUndefined()
+            }
+
+            // Clean up
+            // Note: Not removing session to avoid race with background SessionSummary.summarize
+          }),
+        ),
+      )
     },
     60000,
   )
@@ -72,62 +85,68 @@ describe("StructuredOutput Integration", () => {
   test.skipIf(!hasApiKey)(
     "produces structured output with nested objects",
     async () => {
-      await withInstance(async () => {
-        const session = await Session.create({ title: "Nested Schema Test" })
-
-        const result = await SessionPrompt.prompt({
-          sessionID: session.id,
-          parts: [
-            {
-              type: "text",
-              text: "Tell me about Anthropic company in a structured format.",
-            },
-          ],
-          format: {
-            type: "json_schema",
-            schema: {
-              type: "object",
-              properties: {
-                company: {
+      await withInstance(() =>
+        run(
+          Effect.gen(function* () {
+            const prompt = yield* SessionPrompt.Service
+            const sessions = yield* Session.Service
+            const session = yield* sessions.create({ title: "Nested Schema Test" })
+
+            const result = yield* prompt.prompt({
+              sessionID: session.id,
+              parts: [
+                {
+                  type: "text",
+                  text: "Tell me about Anthropic company in a structured format.",
+                },
+              ],
+              format: {
+                type: "json_schema",
+                schema: {
                   type: "object",
                   properties: {
-                    name: { type: "string" },
-                    founded: { type: "number" },
+                    company: {
+                      type: "object",
+                      properties: {
+                        name: { type: "string" },
+                        founded: { type: "number" },
+                      },
+                      required: ["name", "founded"],
+                    },
+                    products: {
+                      type: "array",
+                      items: { type: "string" },
+                    },
                   },
-                  required: ["name", "founded"],
-                },
-                products: {
-                  type: "array",
-                  items: { type: "string" },
+                  required: ["company"],
                 },
+                retryCount: 0,
               },
-              required: ["company"],
-            },
-            retryCount: 0,
-          },
-        })
-
-        // Verify structured output was captured (only on assistant messages)
-        expect(result.info.role).toBe("assistant")
-        if (result.info.role === "assistant") {
-          expect(result.info.structured).toBeDefined()
-          const output = result.info.structured as any
-
-          expect(output.company).toBeDefined()
-          expect(output.company.name).toBe("Anthropic")
-          expect(typeof output.company.founded).toBe("number")
-
-          if (output.products) {
-            expect(Array.isArray(output.products)).toBe(true)
-          }
-
-          // Verify no error was set
-          expect(result.info.error).toBeUndefined()
-        }
-
-        // Clean up
-        // Note: Not removing session to avoid race with background SessionSummary.summarize
-      })
+            })
+
+            // Verify structured output was captured (only on assistant messages)
+            expect(result.info.role).toBe("assistant")
+            if (result.info.role === "assistant") {
+              expect(result.info.structured).toBeDefined()
+              const output = result.info.structured as any
+
+              expect(output.company).toBeDefined()
+              expect(output.company.name).toBe("Anthropic")
+              expect(typeof output.company.founded).toBe("number")
+
+              if (output.products) {
+                expect(Array.isArray(output.products)).toBe(true)
+              }
+
+              // Verify no error was set
+              expect(result.info.error).toBeUndefined()
+            }
+
+            // Clean up
+            // Note: Not removing session to avoid race with background SessionSummary.summarize
+          }),
+        ),
+      )
     },
     60000,
   )
@@ -135,35 +154,41 @@ describe("StructuredOutput Integration", () => {
   test.skipIf(!hasApiKey)(
     "works with text outputFormat (default)",
     async () => {
-      await withInstance(async () => {
-        const session = await Session.create({ title: "Text Output Test" })
-
-        const result = await SessionPrompt.prompt({
-          sessionID: session.id,
-          parts: [
-            {
-              type: "text",
-              text: "Say hello.",
-            },
-          ],
-          format: {
-            type: "text",
-          },
-        })
-
-        // Verify no structured output (text mode) and no error
-        expect(result.info.role).toBe("assistant")
-        if (result.info.role === "assistant") {
-          expect(result.info.structured).toBeUndefined()
-          expect(result.info.error).toBeUndefined()
-        }
-
-        // Verify we got a response with parts
-        expect(result.parts.length).toBeGreaterThan(0)
-
-        // Clean up
-        // Note: Not removing session to avoid race with background SessionSummary.summarize
-      })
+      await withInstance(() =>
+        run(
+          Effect.gen(function* () {
+            const prompt = yield* SessionPrompt.Service
+            const sessions = yield* Session.Service
+            const session = yield* sessions.create({ title: "Text Output Test" })
+
+            const result = yield* prompt.prompt({
+              sessionID: session.id,
+              parts: [
+                {
+                  type: "text",
+                  text: "Say hello.",
+                },
+              ],
+              format: {
+                type: "text",
+              },
+            })
+
+            // Verify no structured output (text mode) and no error
+            expect(result.info.role).toBe("assistant")
+            if (result.info.role === "assistant") {
+              expect(result.info.structured).toBeUndefined()
+              expect(result.info.error).toBeUndefined()
+            }
+
+            // Verify we got a response with parts
+            expect(result.parts.length).toBeGreaterThan(0)
+
+            // Clean up
+            // Note: Not removing session to avoid race with background SessionSummary.summarize
+          }),
+        ),
+      )
     },
     60000,
   )
@@ -171,47 +196,53 @@ describe("StructuredOutput Integration", () => {
   test.skipIf(!hasApiKey)(
     "stores outputFormat on user message",
     async () => {
-      await withInstance(async () => {
-        const session = await Session.create({ title: "OutputFormat Storage Test" })
-
-        await SessionPrompt.prompt({
-          sessionID: session.id,
-          parts: [
-            {
-              type: "text",
-              text: "What is 1 + 1?",
-            },
-          ],
-          format: {
-            type: "json_schema",
-            schema: {
-              type: "object",
-              properties: {
-                result: { type: "number" },
+      await withInstance(() =>
+        run(
+          Effect.gen(function* () {
+            const prompt = yield* SessionPrompt.Service
+            const sessions = yield* Session.Service
+            const session = yield* sessions.create({ title: "OutputFormat Storage Test" })
+
+            yield* prompt.prompt({
+              sessionID: session.id,
+              parts: [
+                {
+                  type: "text",
+                  text: "What is 1 + 1?",
+                },
+              ],
+              format: {
+                type: "json_schema",
+                schema: {
+                  type: "object",
+                  properties: {
+                    result: { type: "number" },
+                  },
+                  required: ["result"],
+                },
+                retryCount: 3,
               },
-              required: ["result"],
-            },
-            retryCount: 3,
-          },
-        })
-
-        // Get all messages from session
-        const messages = await Session.messages({ sessionID: session.id })
-        const userMessage = messages.find((m) => m.info.role === "user")
-
-        // Verify outputFormat was stored on user message
-        expect(userMessage).toBeDefined()
-        if (userMessage?.info.role === "user") {
-          expect(userMessage.info.format).toBeDefined()
-          expect(userMessage.info.format?.type).toBe("json_schema")
-          if (userMessage.info.format?.type === "json_schema") {
-            expect(userMessage.info.format.retryCount).toBe(3)
-          }
-        }
-
-        // Clean up
-        // Note: Not removing session to avoid race with background SessionSummary.summarize
-      })
+            })
+
+            // Get all messages from session
+            const messages = yield* sessions.messages({ sessionID: session.id })
+            const userMessage = messages.find((m) => m.info.role === "user")
+
+            // Verify outputFormat was stored on user message
+            expect(userMessage).toBeDefined()
+            if (userMessage?.info.role === "user") {
+              expect(userMessage.info.format).toBeDefined()
+              expect(userMessage.info.format?.type).toBe("json_schema")
+              if (userMessage.info.format?.type === "json_schema") {
+                expect(userMessage.info.format.retryCount).toBe(3)
+              }
+            }
+
+            // Clean up
+            // Note: Not removing session to avoid race with background SessionSummary.summarize
+          }),
+        ),
+      )
     },
     60000,
   )

+ 1 - 1
packages/opencode/test/tool/task.test.ts

@@ -76,7 +76,7 @@ function stubOps(opts?: { onPrompt?: (input: SessionPrompt.PromptInput) => void;
   }
 }
 
-function reply(input: Parameters<typeof SessionPrompt.prompt>[0], text: string): MessageV2.WithParts {
+function reply(input: SessionPrompt.PromptInput, text: string): MessageV2.WithParts {
   const id = MessageID.ascending()
   return {
     info: {