Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const sessionHandlers = HttpApiBuilder.group(InstanceHttpApi, "session",
})

const status = Effect.fn("SessionHttpApi.status")(function* () {
return Object.fromEntries(yield* statusSvc.list())
return Object.fromEntries(yield* statusSvc.list({ recursive: true }))
})

const requireSession = Effect.fn("SessionHttpApi.requireSession")(function* (sessionID: SessionID) {
Expand Down
33 changes: 29 additions & 4 deletions packages/opencode/src/session/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { NonNegativeInt } from "@opencode-ai/core/schema"
import { Effect, Layer, Context, Schema } from "effect"
import { EventV2Bridge } from "@/event-v2-bridge"
import { EventV2 } from "@opencode-ai/core/event"
import path from "node:path"

export const Info = Schema.Union([
Schema.Struct({
Expand Down Expand Up @@ -50,7 +51,7 @@ export const Event = {

export interface Interface {
readonly get: (sessionID: SessionID) => Effect.Effect<Info>
readonly list: () => Effect.Effect<Map<SessionID, Info>>
readonly list: (input?: { recursive?: boolean }) => Effect.Effect<Map<SessionID, Info>>
readonly set: (sessionID: SessionID, status: Info) => Effect.Effect<void>
}

Expand All @@ -60,18 +61,37 @@ export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const events = yield* EventV2Bridge.Service
const directories = new Map<string, Map<SessionID, Info>>()

const state = yield* InstanceState.make(
Effect.fn("SessionStatus.state")(() => Effect.succeed(new Map<SessionID, Info>())),
Effect.fn("SessionStatus.state")((ctx) =>
Effect.gen(function* () {
const result = new Map<SessionID, Info>()
directories.set(ctx.directory, result)
yield* Effect.addFinalizer(() => Effect.sync(() => directories.delete(ctx.directory)))
return result
}),
),
)

const get = Effect.fn("SessionStatus.get")(function* (sessionID: SessionID) {
const data = yield* InstanceState.get(state)
return data.get(sessionID) ?? { type: "idle" as const }
})

const list = Effect.fn("SessionStatus.list")(function* () {
return new Map(yield* InstanceState.get(state))
const list = Effect.fn("SessionStatus.list")(function* (input?: { recursive?: boolean }) {
const current = yield* InstanceState.directory
const data = yield* InstanceState.get(state)
if (!input?.recursive) return new Map(data)

const result = new Map<SessionID, Info>()
for (const [directory, statuses] of directories) {
if (!isSameOrChildDirectory(current, directory)) continue
for (const [sessionID, status] of statuses) {
result.set(sessionID, status)
}
}
return result
})

const set = Effect.fn("SessionStatus.set")(function* (sessionID: SessionID, status: Info) {
Expand All @@ -89,6 +109,11 @@ export const layer = Layer.effect(
}),
)

function isSameOrChildDirectory(parent: string, directory: string) {
const relative = path.relative(parent, directory)
return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative))
}

export const defaultLayer = layer.pipe(Layer.provide(EventV2Bridge.defaultLayer))

export * as SessionStatus from "./status"
57 changes: 56 additions & 1 deletion packages/opencode/test/server/httpapi-sdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { TestLLMServer } from "../lib/llm-server"
import path from "path"
import { resetDatabase } from "../fixture/db"
import { disposeAllInstances, TestInstance, tmpdirScoped } from "../fixture/fixture"
import { awaitWithTimeout, testEffect } from "../lib/effect"
import { awaitWithTimeout, pollWithTimeout, testEffect } from "../lib/effect"
import { testProviderConfig } from "../lib/test-provider"
import { ProviderV2 } from "@opencode-ai/core/provider"
import { Database } from "@opencode-ai/core/database/database"
Expand Down Expand Up @@ -576,6 +576,61 @@ describe("HttpApi SDK", () => {
),
)

serverPathParity("aggregates session status from child directories", (serverPath) =>
withFakeLlmProject(serverPath, { setup: writeStandardFiles }, ({ sdk, directory, llm }) =>
Effect.gen(function* () {
const childDirectory = path.join(directory, "backend")
yield* writeStandardFiles(childDirectory)

const childSdk = yield* client(serverPath, childDirectory)
const child = yield* capture(() => childSdk.session.create({ title: "child" }))
const childID = String(record(child.data).id)

const releasePrompt = yield* Deferred.make<void>()
yield* llm.hold("done", Effect.runPromise(Deferred.await(releasePrompt)))
const asyncPrompt = yield* capture(() =>
childSdk.session.promptAsync({
sessionID: childID,
agent: "build",
model: { providerID: "test", modelID: "test-model" },
parts: [{ type: "text", text: "keep running" }],
}),
)
expect(asyncPrompt.status).toBe(204)
yield* awaitWithTimeout(llm.wait(1), "timed out waiting for child prompt request", "5 seconds")
yield* pollWithTimeout(
capture(() => childSdk.session.status()).pipe(
Effect.map((result) => (record(result.data)[childID] ? result : undefined)),
),
"timed out waiting for child session status",
"5 seconds",
)

const status = yield* capture(() => sdk.session.status())
const statusMap = record(status.data)
const parentStatusCode = status.status
const parentChildStatus = statusMap[childID]

yield* Deferred.succeed(releasePrompt, void 0)
yield* pollWithTimeout(
capture(() => childSdk.session.status()).pipe(
Effect.map((result) => (record(result.data)[childID] ? undefined : result)),
),
"timed out waiting for child session to finish",
"5 seconds",
)

expect(parentStatusCode).toBe(200)
expect(parentChildStatus).toEqual({ type: "busy" })

return {
status: parentStatusCode,
childStatus: parentChildStatus,
}
}),
),
)

serverPathParity("matches generated SDK session message and part routes", (serverPath) =>
withStandardProject(serverPath, ({ sdk, directory }) =>
Effect.gen(function* () {
Expand Down
Loading