diff --git a/.changeset/calm-background-process-scans.md b/.changeset/calm-background-process-scans.md new file mode 100644 index 00000000000..3b32864b9fa --- /dev/null +++ b/.changeset/calm-background-process-scans.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Skip inferred background-process port scanning in VS Code sessions to avoid unnecessary Bun subprocess polling. diff --git a/packages/opencode/src/kilocode/background-process/index.ts b/packages/opencode/src/kilocode/background-process/index.ts index b347d67e92c..cafef91183c 100644 --- a/packages/opencode/src/kilocode/background-process/index.ts +++ b/packages/opencode/src/kilocode/background-process/index.ts @@ -8,6 +8,7 @@ import { SessionID } from "@/session/schema" import { Shell } from "@/shell/shell" import { NonNegativeInt, PositiveInt, optionalOmitUndefined, withStatics } from "@/util/schema" import { zod, ZodOverride } from "@/util/effect-zod" +import { Flag } from "@opencode-ai/core/flag/flag" import * as Log from "@opencode-ai/core/util/log" import { spawn, type ChildProcess } from "child_process" import { Context, Effect, Layer, Schema, Types } from "effect" @@ -172,7 +173,8 @@ export namespace BackgroundProcess { return changed } const fallback = active.info.ready && active.start.ready?.port ? [active.start.ready.port] : [] - const next = Array.from(new Set([...(await Ports.list(pid)), ...fallback])).toSorted((a, b) => a - b) + const ports = Flag.KILO_CLIENT === "cli" ? await Ports.list(pid) : [] + const next = Array.from(new Set([...ports, ...fallback])).toSorted((a, b) => a - b) if (same(active.info.ports, next)) return false active.info.ports = next active.info.time.updated = Date.now() @@ -209,6 +211,7 @@ export namespace BackgroundProcess { function poll(active: Active) { if (active.disposed) return + if (Flag.KILO_CLIENT !== "cli") return if (terminal(active.info.status)) return if (active.poll) return active.poll = setTimeout(() => { diff --git a/packages/opencode/test/kilocode/background-process.test.ts b/packages/opencode/test/kilocode/background-process.test.ts index 911f5ac8a88..0e785e4485b 100644 --- a/packages/opencode/test/kilocode/background-process.test.ts +++ b/packages/opencode/test/kilocode/background-process.test.ts @@ -23,6 +23,14 @@ async function script(dir: string, name: string, source: string) { return `${bin} ${arg}` } +function port() { + const server = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response() }) + const port = server.port + server.stop(true) + if (!port) throw new Error("Failed to reserve port") + return port +} + function update(sessionID: SessionID) { const state: { off?: () => void; timer?: ReturnType } = {} const promise = new Promise((resolve, reject) => { @@ -92,6 +100,119 @@ setInterval(() => {}, 1_000) }), ) + it.instance("reports explicit readiness ports for VS Code clients", () => + Effect.gen(function* () { + const test = yield* TestInstance + const sessionID = SessionID.descending() + const listen = port() + const command = yield* Effect.promise(() => + script( + test.directory, + "vscode-ready-port.mjs", + `Bun.serve({ hostname: "127.0.0.1", port: ${listen}, fetch: () => new Response() }) +`, + ), + ) + const client = process.env["KILO_CLIENT"] + process.env["KILO_CLIENT"] = "vscode" + + try { + const info = yield* Effect.promise(() => + BackgroundProcess.start({ + sessionID, + command, + cwd: test.directory, + ready: { port: listen, timeout: 5_000 }, + }), + ) + + expect(info.status).toBe("ready") + expect(info.ports).toEqual([listen]) + } finally { + yield* Effect.promise(() => BackgroundProcess.stopSession(sessionID)) + if (client === undefined) delete process.env["KILO_CLIENT"] + else process.env["KILO_CLIENT"] = client + } + }), + ) + + it.instance("infers ports for CLI clients", () => + Effect.gen(function* () { + const test = yield* TestInstance + const sessionID = SessionID.descending() + const listen = port() + const command = yield* Effect.promise(() => + script( + test.directory, + "cli-port.mjs", + `Bun.serve({ hostname: "127.0.0.1", port: ${listen}, fetch: () => new Response() }) +`, + ), + ) + const client = process.env["KILO_CLIENT"] + process.env["KILO_CLIENT"] = "cli" + + try { + const info = yield* Effect.promise(() => + BackgroundProcess.start({ + sessionID, + command, + cwd: test.directory, + }), + ) + + let found = yield* Effect.promise(() => BackgroundProcess.get(info.id)) + if (process.platform !== "win32") { + for (let attempt = 0; attempt < 40 && !found?.ports.includes(listen); attempt++) { + yield* Effect.promise(() => Bun.sleep(250)) + found = yield* Effect.promise(() => BackgroundProcess.get(info.id)) + } + } + expect(found?.ports).toEqual(process.platform === "win32" ? [] : [listen]) + } finally { + yield* Effect.promise(() => BackgroundProcess.stopSession(sessionID)) + if (client === undefined) delete process.env["KILO_CLIENT"] + else process.env["KILO_CLIENT"] = client + } + }), + ) + + it.instance("does not infer ports for VS Code clients", () => + Effect.gen(function* () { + const test = yield* TestInstance + const sessionID = SessionID.descending() + const listen = port() + const command = yield* Effect.promise(() => + script( + test.directory, + "vscode-port.mjs", + `Bun.serve({ hostname: "127.0.0.1", port: ${listen}, fetch: () => new Response() }) +`, + ), + ) + const client = process.env["KILO_CLIENT"] + process.env["KILO_CLIENT"] = "vscode" + + try { + const info = yield* Effect.promise(() => + BackgroundProcess.start({ + sessionID, + command, + cwd: test.directory, + }), + ) + + yield* Effect.promise(() => Bun.sleep(2_500)) + const found = yield* Effect.promise(() => BackgroundProcess.get(info.id)) + expect(found?.ports).toEqual([]) + } finally { + yield* Effect.promise(() => BackgroundProcess.stopSession(sessionID)) + if (client === undefined) delete process.env["KILO_CLIENT"] + else process.env["KILO_CLIENT"] = client + } + }), + ) + it.instance("publishes output updates from process callbacks", () => Effect.gen(function* () { const test = yield* TestInstance