Quellcode durchsuchen

refactor(session): destroy SessionRunState facade (#22064)

Kit Langton vor 5 Monaten
Ursprung
Commit
1a509d62a0

+ 45 - 0
packages/opencode/specs/effect-migration.md

@@ -217,6 +217,7 @@ Fully migrated (single namespace, InstanceState where needed, flattened facade):
 - [x] `SessionSummary` — `session/summary.ts`
 - [x] `SessionRevert` — `session/revert.ts`
 - [x] `Instruction` — `session/instruction.ts`
+- [x] `SystemPrompt` — `session/system.ts`
 - [x] `Provider` — `provider/provider.ts`
 - [x] `Storage` — `storage/storage.ts`
 - [x] `ShareNext` — `share/share-next.ts`
@@ -340,3 +341,47 @@ For each service, the migration is roughly:
 - `ShareNext` — migrated 2026-04-11. Swapped remaining async callers to `AppRuntime.runPromise(ShareNext.Service.use(...))`, removed the `makeRuntime(...)` facade, and kept instance bootstrap on the shared app runtime.
 - `SessionTodo` — migrated 2026-04-10. Already matched the target service shape in `session/todo.ts`: single namespace, traced Effect methods, and no `makeRuntime(...)` facade remained; checklist updated to reflect the completed migration.
 - `Storage` — migrated 2026-04-10. One production caller (`Session.diff`) and all storage.test.ts tests converted to effectful style. Facades and `makeRuntime` removed.
+- `SessionRunState` — migrated 2026-04-11. Single caller in `server/routes/session.ts` converted; facade removed.
+- `Account` — migrated 2026-04-11. Callers in `server/routes/experimental.ts` and `cli/cmd/account.ts` converted; facade removed.
+- `Instruction` — migrated 2026-04-11. Test-only callers converted; facade removed.
+- `FileTime` — migrated 2026-04-11. Test-only callers converted; facade removed.
+- `FileWatcher` — migrated 2026-04-11. Callers in `project/bootstrap.ts` and test converted; facade removed.
+- `Question` — migrated 2026-04-11. Callers in `server/routes/question.ts` and test converted; facade removed.
+- `Truncate` — migrated 2026-04-11. Caller in `tool/tool.ts` and test converted; facade removed.
+
+## Route handler effectification
+
+Route handlers should wrap their entire body in a single `AppRuntime.runPromise(Effect.gen(...))` call, yielding services from context rather than calling facades one-by-one. This eliminates multiple `runPromise` round-trips and lets handlers compose naturally.
+
+```ts
+// Before — one facade call per service
+async (c) => {
+  await SessionRunState.assertNotBusy(id)
+  await Session.removeMessage({ sessionID: id, messageID })
+  return c.json(true)
+}
+
+// After — one Effect.gen, yield services from context
+async (c) => {
+  await AppRuntime.runPromise(
+    Effect.gen(function* () {
+      const state = yield* SessionRunState.Service
+      const session = yield* Session.Service
+      yield* state.assertNotBusy(id)
+      yield* session.removeMessage({ sessionID: id, messageID })
+    }),
+  )
+  return c.json(true)
+}
+```
+
+When migrating, always use `{ concurrency: "unbounded" }` with `Effect.all` — route handlers should run independent service calls in parallel, not sequentially.
+
+Route files to convert (each handler that calls facades should be wrapped):
+
+- [ ] `server/routes/session.ts` — heaviest; uses Session, SessionPrompt, SessionRevert, SessionCompaction, SessionShare, SessionSummary, SessionRunState, Agent, Permission, Bus
+- [ ] `server/routes/global.ts` — uses Config, Project, Provider, Vcs, Snapshot, Agent
+- [ ] `server/routes/provider.ts` — uses Provider, Auth, Config
+- [ ] `server/routes/question.ts` — uses Question
+- [ ] `server/routes/pty.ts` — uses Pty
+- [ ] `server/routes/experimental.ts` — uses Account, ToolRegistry, Agent, MCP, Config

+ 12 - 5
packages/opencode/src/server/instance/session.ts

@@ -13,6 +13,7 @@ import { SessionShare } from "@/share/session"
 import { SessionStatus } from "@/session/status"
 import { SessionSummary } from "@/session/summary"
 import { Todo } from "../../session/todo"
+import { Effect } from "effect"
 import { AppRuntime } from "../../effect/app-runtime"
 import { Agent } from "../../agent/agent"
 import { Snapshot } from "@/snapshot"
@@ -724,11 +725,17 @@ export const SessionRoutes = lazy(() =>
       ),
       async (c) => {
         const params = c.req.valid("param")
-        await SessionRunState.assertNotBusy(params.sessionID)
-        await Session.removeMessage({
-          sessionID: params.sessionID,
-          messageID: params.messageID,
-        })
+        await AppRuntime.runPromise(
+          Effect.gen(function* () {
+            const state = yield* SessionRunState.Service
+            const session = yield* Session.Service
+            yield* state.assertNotBusy(params.sessionID)
+            yield* session.removeMessage({
+              sessionID: params.sessionID,
+              messageID: params.messageID,
+            })
+          }),
+        )
         return c.json(true)
       },
     )

+ 0 - 6
packages/opencode/src/session/run-state.ts

@@ -1,6 +1,5 @@
 import { InstanceState } from "@/effect/instance-state"
 import { Runner } from "@/effect/runner"
-import { makeRuntime } from "@/effect/run-service"
 import { Effect, Layer, Scope, Context } from "effect"
 import { Session } from "."
 import { MessageV2 } from "./message-v2"
@@ -106,9 +105,4 @@ export namespace SessionRunState {
   )
 
   export const defaultLayer = layer.pipe(Layer.provide(SessionStatus.defaultLayer))
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export async function assertNotBusy(sessionID: SessionID) {
-    return runPromise((svc) => svc.assertNotBusy(sessionID))
-  }
 }

+ 2 - 49
packages/opencode/test/server/session-actions.test.ts

@@ -1,11 +1,9 @@
 import { afterEach, describe, expect, mock, spyOn, 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 { ModelID, ProviderID } from "../../src/provider/schema"
-import { MessageID, PartID, type SessionID } from "../../src/session/schema"
 import { SessionPrompt } from "../../src/session/prompt"
-import { SessionRunState } from "../../src/session/run-state"
 import { Log } from "../../src/util/log"
 import { tmpdir } from "../fixture/fixture"
 
@@ -16,25 +14,6 @@ afterEach(async () => {
   await Instance.disposeAll()
 })
 
-async function user(sessionID: SessionID, text: string) {
-  const msg = await Session.updateMessage({
-    id: MessageID.ascending(),
-    role: "user",
-    sessionID,
-    agent: "build",
-    model: { providerID: ProviderID.make("test"), modelID: ModelID.make("test") },
-    time: { created: Date.now() },
-  })
-  await Session.updatePart({
-    id: PartID.ascending(),
-    sessionID,
-    messageID: msg.id,
-    type: "text",
-    text,
-  })
-  return msg
-}
-
 describe("session action routes", () => {
   test("abort route calls SessionPrompt.cancel", async () => {
     await using tmp = await tmpdir({ git: true })
@@ -45,9 +24,7 @@ describe("session action routes", () => {
         const cancel = spyOn(SessionPrompt, "cancel").mockResolvedValue()
         const app = Server.Default().app
 
-        const res = await app.request(`/session/${session.id}/abort`, {
-          method: "POST",
-        })
+        const res = await app.request(`/session/${session.id}/abort`, { method: "POST" })
 
         expect(res.status).toBe(200)
         expect(await res.json()).toBe(true)
@@ -57,28 +34,4 @@ describe("session action routes", () => {
       },
     })
   })
-
-  test("delete message route returns 400 when session is busy", async () => {
-    await using tmp = await tmpdir({ git: true })
-    await Instance.provide({
-      directory: tmp.path,
-      fn: async () => {
-        const session = await Session.create({})
-        const msg = await user(session.id, "hello")
-        const busy = spyOn(SessionRunState, "assertNotBusy").mockRejectedValue(new Session.BusyError(session.id))
-        const remove = spyOn(Session, "removeMessage").mockResolvedValue(msg.id)
-        const app = Server.Default().app
-
-        const res = await app.request(`/session/${session.id}/message/${msg.id}`, {
-          method: "DELETE",
-        })
-
-        expect(res.status).toBe(400)
-        expect(busy).toHaveBeenCalledWith(session.id)
-        expect(remove).not.toHaveBeenCalled()
-
-        await Session.remove(session.id)
-      },
-    })
-  })
 })