mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-11 16:52:16 +03:00
fix(server): stop form polling from booting idle locations (#59083)
Co-authored-by: Kit Langton <kit.langton@gmail.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
61163cb134
commit
c8cf3a544f
@@ -2,7 +2,7 @@ import { buildLocationServiceMap } from "../location-services.js"
|
||||
import { LocationServiceMap } from "../location-service-map.js"
|
||||
import { LayerNode } from "@opencode/util/effect/layer-node"
|
||||
import { makeGlobalNode } from "@opencode/util/effect/app-node"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { Effect, Layer, RcMap } from "effect"
|
||||
import { Instance } from "../instance/service.js"
|
||||
|
||||
const instances = makeGlobalNode({
|
||||
@@ -13,6 +13,7 @@ const instances = makeGlobalNode({
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
return Instance.Service.of({
|
||||
provide: (session) => Effect.provide(locations.get(session.location)),
|
||||
live: (session) => RcMap.has(locations.rcMap, LocationServiceMap.canonical(session.location)),
|
||||
})
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface Interface {
|
||||
readonly provide: (
|
||||
session: Session.Info,
|
||||
) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | Error, Exclude<R, Services>>
|
||||
/** Whether `provide` would reuse built or booting capabilities. Selectors without it are assumed live. */
|
||||
readonly live?: (session: Session.Info) => Effect.Effect<boolean>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Instance") {}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { InstancePlugins } from "@opencode/core/plugin/instance"
|
||||
import type { Session } from "@opencode/schema/session"
|
||||
import { makeGlobalNode } from "@opencode/util/effect/app-node"
|
||||
import type { LayerNode } from "@opencode/util/effect/layer-node"
|
||||
import { Context, Duration, Effect, Layer, LayerMap, Scope } from "effect"
|
||||
import { Context, Duration, Effect, Layer, LayerMap, RcMap, Scope } from "effect"
|
||||
|
||||
export interface Configuration {
|
||||
readonly plugins: InstancePlugins.List
|
||||
@@ -101,7 +101,7 @@ export function layer(options: Options, replacements: () => LayerNode.Replacemen
|
||||
),
|
||||
{ idleTimeToLive: Duration.infinity },
|
||||
)
|
||||
return Instance.Service.of({ provide })
|
||||
return Instance.Service.of({ provide, live: (session) => RcMap.has(instances.rcMap, key(session)) })
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { LocationServiceMap } from "@opencode/core/location-services"
|
||||
import { Session } from "@opencode/core/session"
|
||||
import { LocationNotFoundError, InvalidRequestError, SessionNotFoundError } from "@opencode/protocol/errors"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { HttpRouter, HttpServerRequest } from "effect/http"
|
||||
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http"
|
||||
import { HttpApiMiddleware } from "effect/http-api"
|
||||
import { locationErrors, requestRef, sessionInfo, type LocationServices } from "../location"
|
||||
|
||||
@@ -21,7 +21,7 @@ export const formLocationLayer = Layer.effect(
|
||||
const instances = yield* Instance.Service
|
||||
const locations = yield* LocationServiceMap.Service
|
||||
|
||||
return FormLocationMiddleware.of((effect) =>
|
||||
return FormLocationMiddleware.of((effect, options) =>
|
||||
Effect.gen(function* () {
|
||||
const route = yield* HttpRouter.RouteContext
|
||||
if (route.params.sessionID === "global") {
|
||||
@@ -33,6 +33,10 @@ export const formLocationLayer = Layer.effect(
|
||||
}
|
||||
|
||||
const session = yield* sessionInfo(sessions, route.params.sessionID)
|
||||
// Forms live in memory, so a Session without live capabilities has none pending. Listing must
|
||||
// not boot its Location: clients poll whole subagent trees, including deleted worktrees.
|
||||
if (options.endpoint.identifier === "session.form.list" && instances.live && !(yield* instances.live(session)))
|
||||
return HttpServerResponse.jsonUnsafe({ data: [] })
|
||||
return yield* effect.pipe(instances.provide(session), locationErrors)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -42,7 +42,6 @@ it.live("returns LocationNotFoundError for a missing folder and recovers once it
|
||||
"/api/model",
|
||||
"/api/integration",
|
||||
`/api/session/${session.id}/permission`,
|
||||
`/api/session/${session.id}/form`,
|
||||
"/api/session/global/form",
|
||||
]
|
||||
for (const endpoint of endpoints) {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { expect } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import { Effect } from "effect"
|
||||
import { tmpdirScoped } from "../../core/test/fixture/tmpdir"
|
||||
import { it } from "../../core/test/lib/effect"
|
||||
import { ServerFetch } from "../src/fetch"
|
||||
|
||||
it.live("lists no forms for a Session whose Location is not live, and pending forms once it is", () =>
|
||||
Effect.gen(function* () {
|
||||
const handler = yield* ServerFetch.make({
|
||||
app: { version: "test" },
|
||||
database: { path: ":memory:" },
|
||||
fs: { filewatcher: false },
|
||||
models: { fetch: false },
|
||||
})
|
||||
const request = (path: string, body?: unknown) =>
|
||||
Effect.promise(async () => {
|
||||
const response = await handler(
|
||||
new Request(`http://opencode.local${path}`, {
|
||||
method: body === undefined ? "GET" : "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
}),
|
||||
)
|
||||
return { status: response.status, body: await response.json() }
|
||||
})
|
||||
const create = (directory: string) =>
|
||||
request("/api/session", { location: { directory } }).pipe(
|
||||
Effect.map((response) => (response.body as { data: { id: string } }).data.id),
|
||||
)
|
||||
|
||||
// A subagent whose worktree was deleted must not boot or 404.
|
||||
const deleted = yield* tmpdirScoped()
|
||||
const orphan = yield* create(deleted.path)
|
||||
yield* Effect.promise(() => fs.rm(deleted.path, { recursive: true, force: true }))
|
||||
expect(yield* request(`/api/session/${orphan}/form`)).toEqual({ status: 200, body: { data: [] } })
|
||||
|
||||
const directory = yield* tmpdirScoped()
|
||||
const live = yield* create(directory.path)
|
||||
const created = yield* request(`/api/session/${live}/form`, {
|
||||
title: "Pick one",
|
||||
fields: [{ key: "answer", type: "string" }],
|
||||
})
|
||||
expect(created.status).toBe(200)
|
||||
expect(yield* request(`/api/session/${live}/form`)).toEqual({
|
||||
status: 200,
|
||||
body: { data: [(created.body as { data: unknown }).data] },
|
||||
})
|
||||
}).pipe(Effect.scoped),
|
||||
)
|
||||
Reference in New Issue
Block a user