diff --git a/packages/opencode/src/kilocode/project-id.ts b/packages/opencode/src/kilocode/project-id.ts index 02d016d9fd..d0dc68b949 100644 --- a/packages/opencode/src/kilocode/project-id.ts +++ b/packages/opencode/src/kilocode/project-id.ts @@ -1,6 +1,6 @@ -import { Context, Effect, Layer } from "effect" +import { Cache, Context, Effect, Layer } from "effect" import { Instance } from "@/kilocode/instance" -import { InstanceState } from "@/effect/instance-state" +import { registerDisposer } from "@/effect/instance-registry" import { makeRuntime } from "@/effect/run-service" import path from "path" import { $ } from "bun" @@ -83,9 +83,7 @@ async function getProjectIdFromGit(directory: string): Promise .kilocode/config.json -> git origin URL * @returns Normalized project ID or undefined */ -async function resolveProjectId(): Promise { - const dir = Instance.directory - +async function resolveProjectId(dir: string): Promise { // Priority 1: .kilo/config.json (falls back to .kilocode/config.json) const id = await getProjectIdFromConfig(dir) if (id) return id @@ -104,13 +102,15 @@ export namespace KiloProjectID { export const layer = Layer.effect( Service, Effect.gen(function* () { - const state = yield* InstanceState.make( - Effect.fn("KiloProjectID.state")(function* () { - return { id: yield* Effect.promise(() => resolveProjectId()) } - }), - ) + const cache = yield* Cache.make({ + capacity: Number.POSITIVE_INFINITY, + lookup: (directory) => Effect.promise(() => resolveProjectId(directory)), + }) + const off = registerDisposer((directory) => Effect.runPromise(Cache.invalidate(cache, directory))) + yield* Effect.addFinalizer(() => Effect.sync(off)) + return Service.of({ - get: () => InstanceState.use(state, (s) => s.id), + get: () => Cache.get(cache, Instance.directory), }) }), ) diff --git a/packages/opencode/test/kilocode/project-id.test.ts b/packages/opencode/test/kilocode/project-id.test.ts index d93dc7a85d..5d59b7d242 100644 --- a/packages/opencode/test/kilocode/project-id.test.ts +++ b/packages/opencode/test/kilocode/project-id.test.ts @@ -2,8 +2,9 @@ import { test, expect, describe } from "bun:test" import { tmpdir } from "../fixture/fixture" import path from "path" import fs from "fs/promises" -import { provideTestInstance } from "../fixture/fixture" +import { provideTestInstance, withTestInstance } from "../fixture/fixture" import { getKiloProjectId } from "../../src/kilocode/project-id" +import { disposeInstance } from "../../src/effect/instance-registry" describe("project-id", () => { describe("normalization", () => { @@ -365,26 +366,49 @@ describe("project-id", () => { }) describe("caching", () => { - test("caches project ID per Instance", async () => { - await using tmp = await tmpdir({ - git: true, + test("keeps project IDs isolated across active project contexts", async () => { + await using first = await tmpdir({ init: async (dir) => { - await Bun.$`git remote add origin https://github.com/Kilo-Org/handbook.git`.cwd(dir).quiet() + await fs.mkdir(path.join(dir, ".kilo"), { recursive: true }) + await Bun.write(path.join(dir, ".kilo", "config.json"), JSON.stringify({ project: { id: "first" } })) + }, + }) + await using second = await tmpdir({ + init: async (dir) => { + await fs.mkdir(path.join(dir, ".kilo"), { recursive: true }) + await Bun.write(path.join(dir, ".kilo", "config.json"), JSON.stringify({ project: { id: "second" } })) }, }) - const id1 = await provideTestInstance({ - directory: tmp.path, - fn: () => getKiloProjectId(), + const ids = await Promise.all([ + withTestInstance({ directory: first.path, fn: () => getKiloProjectId() }), + withTestInstance({ directory: second.path, fn: () => getKiloProjectId() }), + ]) + + expect(ids).toEqual(["first", "second"]) + }) + + test("invalidates the cached project ID when the instance is disposed", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await fs.mkdir(path.join(dir, ".kilo"), { recursive: true }) + await Bun.write(path.join(dir, ".kilo", "config.json"), JSON.stringify({ project: { id: "first" } })) + }, }) - const id2 = await provideTestInstance({ + const ids = await provideTestInstance({ directory: tmp.path, - fn: () => getKiloProjectId(), + fn: async (ctx) => { + const first = await getKiloProjectId() + await Bun.write(path.join(tmp.path, ".kilo", "config.json"), JSON.stringify({ project: { id: "second" } })) + const cached = await getKiloProjectId() + await disposeInstance(ctx.directory) + const refreshed = await getKiloProjectId() + return { first, cached, refreshed } + }, }) - expect(id1).toBe(id2) - expect(id1).toBe("handbook") + expect(ids).toEqual({ first: "first", cached: "first", refreshed: "second" }) }) }) diff --git a/script/architecture-allowlist.json b/script/architecture-allowlist.json index 9ff61dc3bd..024a5c6a04 100644 --- a/script/architecture-allowlist.json +++ b/script/architecture-allowlist.json @@ -11,7 +11,6 @@ "packages/opencode/src/kilocode/background-process/index.ts": { "count": 1, "owner": "process-runtime", "reason": "Directory-keyed background process registry" }, "packages/opencode/src/kilocode/interactive-terminal/index.ts": { "count": 1, "owner": "terminal-runtime", "reason": "Interactive terminal manager state" }, "packages/opencode/src/kilocode/notebook/service.ts": { "count": 1, "owner": "notebook-runtime", "reason": "Notebook cell execution service state" }, - "packages/opencode/src/kilocode/project-id.ts": { "count": 1, "owner": "project-runtime", "reason": "Cached project identifier resolution" }, "packages/opencode/src/kilocode/watcher.ts": { "count": 1, "owner": "watcher-runtime", "reason": "Eager location watcher subscription" } } },