From 10a3d6c54e403ec68c0ef150c2f109462199df23 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 21 Mar 2026 19:55:07 -0400 Subject: [PATCH] effectify SessionStatus service (#18565) --- packages/opencode/specs/effect-migration.md | 1 + .../opencode/src/server/routes/session.ts | 4 +- packages/opencode/src/session/processor.ts | 6 +- packages/opencode/src/session/prompt.ts | 10 +-- packages/opencode/src/session/status.ts | 78 ++++++++++++------- 5 files changed, 61 insertions(+), 38 deletions(-) diff --git a/packages/opencode/specs/effect-migration.md b/packages/opencode/specs/effect-migration.md index 80c906fcc..2a5b289ca 100644 --- a/packages/opencode/specs/effect-migration.md +++ b/packages/opencode/specs/effect-migration.md @@ -123,6 +123,7 @@ Fully migrated (single namespace, InstanceState where needed, flattened facade): - [x] `Truncate` — `tool/truncate.ts` - [x] `Vcs` — `project/vcs.ts` - [x] `Discovery` — `skill/discovery.ts` +- [x] `SessionStatus` Still open and likely worth migrating: diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts index e399636ad..abc820c2a 100644 --- a/packages/opencode/src/server/routes/session.ts +++ b/packages/opencode/src/server/routes/session.ts @@ -88,8 +88,8 @@ export const SessionRoutes = lazy(() => }, }), async (c) => { - const result = SessionStatus.list() - return c.json(result) + const result = await SessionStatus.list() + return c.json(Object.fromEntries(result)) }, ) .get( diff --git a/packages/opencode/src/session/processor.ts b/packages/opencode/src/session/processor.ts index c3a572f5b..ccb09e71a 100644 --- a/packages/opencode/src/session/processor.ts +++ b/packages/opencode/src/session/processor.ts @@ -57,7 +57,7 @@ export namespace SessionProcessor { input.abort.throwIfAborted() switch (value.type) { case "start": - SessionStatus.set(input.sessionID, { type: "busy" }) + await SessionStatus.set(input.sessionID, { type: "busy" }) break case "reasoning-start": @@ -368,7 +368,7 @@ export namespace SessionProcessor { if (retry !== undefined) { attempt++ const delay = SessionRetry.delay(attempt, error.name === "APIError" ? error : undefined) - SessionStatus.set(input.sessionID, { + await SessionStatus.set(input.sessionID, { type: "retry", attempt, message: retry, @@ -382,7 +382,7 @@ export namespace SessionProcessor { sessionID: input.assistantMessage.sessionID, error: input.assistantMessage.error, }) - SessionStatus.set(input.sessionID, { type: "idle" }) + await SessionStatus.set(input.sessionID, { type: "idle" }) } } if (snapshot) { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 5625c571c..8a1081748 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -257,17 +257,17 @@ export namespace SessionPrompt { return s[sessionID].abort.signal } - export function cancel(sessionID: SessionID) { + export async function cancel(sessionID: SessionID) { log.info("cancel", { sessionID }) const s = state() const match = s[sessionID] if (!match) { - SessionStatus.set(sessionID, { type: "idle" }) + await SessionStatus.set(sessionID, { type: "idle" }) return } match.abort.abort() delete s[sessionID] - SessionStatus.set(sessionID, { type: "idle" }) + await SessionStatus.set(sessionID, { type: "idle" }) return } @@ -286,7 +286,7 @@ export namespace SessionPrompt { }) } - using _ = defer(() => cancel(sessionID)) + await using _ = defer(() => cancel(sessionID)) // Structured output state // Note: On session resumption, state is reset but outputFormat is preserved @@ -296,7 +296,7 @@ export namespace SessionPrompt { let step = 0 const session = await Session.get(sessionID) while (true) { - SessionStatus.set(sessionID, { type: "busy" }) + await SessionStatus.set(sessionID, { type: "busy" }) log.info("loop", { step, sessionID }) if (abort.aborted) break let msgs = await MessageV2.filterCompacted(MessageV2.stream(sessionID)) diff --git a/packages/opencode/src/session/status.ts b/packages/opencode/src/session/status.ts index 57e793985..462d5ded4 100644 --- a/packages/opencode/src/session/status.ts +++ b/packages/opencode/src/session/status.ts @@ -1,7 +1,9 @@ import { BusEvent } from "@/bus/bus-event" import { Bus } from "@/bus" -import { Instance } from "@/project/instance" +import { InstanceState } from "@/effect/instance-state" +import { makeRunPromise } from "@/effect/run-service" import { SessionID } from "./schema" +import { Effect, Layer, ServiceMap } from "effect" import z from "zod" export namespace SessionStatus { @@ -42,36 +44,56 @@ export namespace SessionStatus { ), } - const state = Instance.state(() => { - const data: Record = {} - return data - }) - - export function get(sessionID: SessionID) { - return ( - state()[sessionID] ?? { - type: "idle", - } - ) + export interface Interface { + readonly get: (sessionID: SessionID) => Effect.Effect + readonly list: () => Effect.Effect> + readonly set: (sessionID: SessionID, status: Info) => Effect.Effect } - export function list() { - return state() - } + export class Service extends ServiceMap.Service()("@opencode/SessionStatus") {} - export function set(sessionID: SessionID, status: Info) { - Bus.publish(Event.Status, { - sessionID, - status, - }) - if (status.type === "idle") { - // deprecated - Bus.publish(Event.Idle, { - sessionID, + export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const state = yield* InstanceState.make( + Effect.fn("SessionStatus.state")(() => Effect.succeed(new Map())), + ) + + const get = Effect.fn("SessionStatus.get")(function* (sessionID: SessionID) { + const data = yield* InstanceState.get(state) + return data.get(sessionID) ?? { type: "idle" as const } }) - delete state()[sessionID] - return - } - state()[sessionID] = status + + const list = Effect.fn("SessionStatus.list")(function* () { + return new Map(yield* InstanceState.get(state)) + }) + + const set = Effect.fn("SessionStatus.set")(function* (sessionID: SessionID, status: Info) { + const data = yield* InstanceState.get(state) + yield* Effect.promise(() => Bus.publish(Event.Status, { sessionID, status })) + if (status.type === "idle") { + yield* Effect.promise(() => Bus.publish(Event.Idle, { sessionID })) + data.delete(sessionID) + return + } + data.set(sessionID, status) + }) + + return Service.of({ get, list, set }) + }), + ) + + const runPromise = makeRunPromise(Service, layer) + + export async function get(sessionID: SessionID) { + return runPromise((svc) => svc.get(sessionID)) + } + + export async function list() { + return runPromise((svc) => svc.list()) + } + + export async function set(sessionID: SessionID, status: Info) { + return runPromise((svc) => svc.set(sessionID, status)) } }