From 344a6a5f0f8377d8ab38792e6141d08947a7dc19 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 18 Jun 2026 11:42:15 +0200 Subject: [PATCH] fix(cli): share listener session runtime --- .changeset/share-server-session-runtime.md | 6 ++ .../src/kilocode/server/httpapi/server.ts | 26 +++++++ .../opencode/src/kilocode/server/listener.ts | 8 +++ .../routes/instance/httpapi/handlers/v2.ts | 2 +- .../server/routes/instance/httpapi/server.ts | 15 ++++- packages/opencode/src/server/server.ts | 5 +- .../kilocode/server/listener-runtime.test.ts | 67 +++++++++++++++++++ script/check-opencode-promise-facades.ts | 1 + 8 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 .changeset/share-server-session-runtime.md create mode 100644 packages/opencode/src/kilocode/server/listener.ts create mode 100644 packages/opencode/test/kilocode/server/listener-runtime.test.ts diff --git a/.changeset/share-server-session-runtime.md b/.changeset/share-server-session-runtime.md new file mode 100644 index 0000000000..16bcb87558 --- /dev/null +++ b/.changeset/share-server-session-runtime.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Keep server controls and events connected to active sessions and subagents. diff --git a/packages/opencode/src/kilocode/server/httpapi/server.ts b/packages/opencode/src/kilocode/server/httpapi/server.ts index e8e1e97879..3c6078e9d1 100644 --- a/packages/opencode/src/kilocode/server/httpapi/server.ts +++ b/packages/opencode/src/kilocode/server/httpapi/server.ts @@ -1,4 +1,10 @@ import { Layer } from "effect" +import { FetchHttpClient, HttpMiddleware, HttpRouter, HttpServer } from "effect/unstable/http" +import { CorsConfig, isAllowedCorsOrigin, type CorsOptions } from "@/server/cors" +import { compressionLayer } from "@/server/routes/instance/httpapi/middleware/compression" +import { corsVaryFix } from "@/server/routes/instance/httpapi/middleware/cors-vary" +import { errorLayer } from "@/server/routes/instance/httpapi/middleware/error" +import { fenceLayer } from "@/server/routes/instance/httpapi/middleware/fence" import { agentBuilderHandlers } from "./handlers/agent-builder" import { backgroundProcessHandlers } from "./handlers/background-process" @@ -29,3 +35,23 @@ export const provide = Layer.provide([ suggestionHandlers, telemetryHandlers, ]) + +export function provideListener(opts?: CorsOptions) { + const cors = HttpRouter.middleware( + HttpMiddleware.cors({ + allowedOrigins: (origin) => isAllowedCorsOrigin(origin, opts), + maxAge: 86_400, + }), + { global: true }, + ) + return Layer.provide([ + errorLayer, + compressionLayer, + corsVaryFix, + fenceLayer, + cors, + FetchHttpClient.layer, + HttpServer.layerServices, + Layer.succeed(CorsConfig)(opts), + ]) +} diff --git a/packages/opencode/src/kilocode/server/listener.ts b/packages/opencode/src/kilocode/server/listener.ts new file mode 100644 index 0000000000..3981a96b29 --- /dev/null +++ b/packages/opencode/src/kilocode/server/listener.ts @@ -0,0 +1,8 @@ +import { AppLayer } from "@/effect/app-runtime" +import { memoMap } from "@opencode-ai/core/effect/memo-map" +import { Layer, Scope } from "effect" + +export function build(layer: Layer.Layer, scope: Scope.Scope) { + // Keep listener transport state fresh while AppLayer reuses the process-wide services. + return Layer.buildWithMemoMap(Layer.fresh(layer).pipe(Layer.provide(AppLayer)), memoMap, scope) +} diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts index daa799b7a8..c110d06b36 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/v2.ts @@ -8,5 +8,5 @@ import { sessionHandlers } from "./v2/session" export const v2Handlers = Layer.mergeAll(sessionHandlers, messageHandlers, modelHandlers, providerHandlers).pipe( Layer.provide(v2LocationLayer), - Layer.provide(SessionV2.defaultLayer), + Layer.provide(SessionV2.layer), // kilocode_change - use the application EventV2Bridge ) diff --git a/packages/opencode/src/server/routes/instance/httpapi/server.ts b/packages/opencode/src/server/routes/instance/httpapi/server.ts index 5f426cf138..d8d7dcd54f 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/server.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/server.ts @@ -82,7 +82,12 @@ import { syncHandlers } from "./handlers/sync" import { tuiHandlers } from "./handlers/tui" import { v2Handlers } from "./handlers/v2" import { workspaceHandlers } from "./handlers/workspace" -import { provide as provideKiloHttpApiHandlers } from "@/kilocode/server/httpapi/server" // kilocode_change +// kilocode_change start +import { + provide as provideKiloHttpApiHandlers, + provideListener as provideKiloListenerRoutes, +} from "@/kilocode/server/httpapi/server" +// kilocode_change end import { instanceContextLayer, instanceRouterMiddleware } from "./middleware/instance-context" import { workspaceRouterMiddleware, workspaceRoutingLayer } from "./middleware/workspace-routing" import { disposeMiddleware } from "./lifecycle" @@ -248,6 +253,14 @@ export function createRoutes( ) } +// kilocode_change start - keep listener routes local while application services come from AppRuntime +export function createListenerRoutes(corsOptions?: CorsOptions) { + return Layer.mergeAll(rootApiRoutes, eventApiRoutes, instanceRoutes, docRoute, uiRoute).pipe( + provideKiloListenerRoutes(corsOptions), + ) +} +// kilocode_change end + export const routes = createRoutes() export const webHandler = lazy(() => diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 9a448b78d6..81f5e55c4e 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -13,6 +13,7 @@ import { WebSocketTracker } from "./routes/instance/httpapi/websocket-tracker" import { PublicApi } from "./routes/instance/httpapi/public" import type { CorsOptions } from "./cors" import { lazy } from "@/util/lazy" +import * as KiloListener from "@/kilocode/server/listener" // kilocode_change // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85 globalThis.AI_SDK_LOG_WARNINGS = false @@ -101,7 +102,7 @@ const listenEffect: (opts: ListenOptions) => Effect.Effect Scope.close(scope, Exit.void).pipe(Effect.ignore)), Effect.map( diff --git a/packages/opencode/test/kilocode/server/listener-runtime.test.ts b/packages/opencode/test/kilocode/server/listener-runtime.test.ts new file mode 100644 index 0000000000..c3bd6c78bf --- /dev/null +++ b/packages/opencode/test/kilocode/server/listener-runtime.test.ts @@ -0,0 +1,67 @@ +import { afterEach, expect, test } from "bun:test" +import { Flag } from "@opencode-ai/core/flag/flag" +import * as Log from "@opencode-ai/core/util/log" +import { Effect } from "effect" +import { AppRuntime } from "../../../src/effect/app-runtime" +import { InstanceRef } from "../../../src/effect/instance-ref" +import { Server } from "../../../src/server/server" +import { SessionPaths } from "../../../src/server/routes/instance/httpapi/groups/session" +import { SessionRunState } from "../../../src/session/run-state" +import { SessionID } from "../../../src/session/schema" +import { withTimeout } from "../../../src/util/timeout" +import { resetDatabase } from "../../fixture/db" +import { disposeAllInstances, reloadTestInstance, tmpdir } from "../../fixture/fixture" + +void Log.init({ print: false }) + +const previous = { + flag: Flag.KILO_SERVER_PASSWORD, + env: process.env.KILO_SERVER_PASSWORD, +} + +afterEach(async () => { + Flag.KILO_SERVER_PASSWORD = previous.flag + if (previous.env === undefined) delete process.env.KILO_SERVER_PASSWORD + else process.env.KILO_SERVER_PASSWORD = previous.env + await disposeAllInstances() + await resetDatabase() +}) + +test("listener aborts shared session runners", async () => { + Flag.KILO_SERVER_PASSWORD = undefined + delete process.env.KILO_SERVER_PASSWORD + await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) + const ctx = await reloadTestInstance({ directory: tmp.path }) + const sessionID = SessionID.descending() + const started = Promise.withResolvers() + const stopped = Promise.withResolvers() + const running = AppRuntime.runPromise( + SessionRunState.Service.use((state) => + state.ensureRunning( + sessionID, + Effect.interrupt, + Effect.sync(started.resolve).pipe(Effect.andThen(Effect.never), Effect.ensuring(Effect.sync(stopped.resolve))), + ), + ).pipe(Effect.provideService(InstanceRef, ctx)), + ).catch(() => undefined) + + try { + await withTimeout(started.promise, 5_000, "timed out waiting for shared session") + const listener = await Server.listen({ hostname: "127.0.0.1", port: 0 }) + try { + const response = await fetch(new URL(SessionPaths.abort.replace(":sessionID", sessionID), listener.url), { + method: "POST", + headers: { "x-kilo-directory": tmp.path }, + }) + expect(response.status).toBe(200) + await withTimeout(stopped.promise, 5_000, "listener did not interrupt the shared session") + } finally { + await withTimeout(listener.stop(true), 10_000, "timed out cleaning up shared-runtime listener") + } + } finally { + await AppRuntime.runPromise( + SessionRunState.Service.use((state) => state.cancel(sessionID)).pipe(Effect.provideService(InstanceRef, ctx)), + ).catch(() => undefined) + await running + } +}, 20_000) diff --git a/script/check-opencode-promise-facades.ts b/script/check-opencode-promise-facades.ts index ea654c5e51..a231c97b73 100644 --- a/script/check-opencode-promise-facades.ts +++ b/script/check-opencode-promise-facades.ts @@ -40,6 +40,7 @@ const testAllow: Record = { "provider/provider.test.ts": { count: 3, reason: "existing runtime integration test" }, "server/experimental-session-list.test.ts": { count: 2, reason: "Kilo session list integration test" }, "server/httpapi-event.test.ts": { count: 6, reason: "event stream integration test" }, + "kilocode/server/listener-runtime.test.ts": { count: 3, reason: "listener and AppRuntime integration test" }, "session/llm.test.ts": { count: 2, reason: "existing runtime integration test" }, "tool/recall.test.ts": { count: 11, reason: "existing runtime integration test" }, }