refactor(cli): scope project ID cache lifecycle

This commit is contained in:
marius-kilocode
2026-08-19 13:02:28 +02:00
parent 20e14cf4f5
commit ec37e1ef59
3 changed files with 47 additions and 24 deletions
+11 -11
View File
@@ -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<string | undefine
* Resolve project ID with priority: .kilo/config.json -> .kilocode/config.json -> git origin URL
* @returns Normalized project ID or undefined
*/
async function resolveProjectId(): Promise<string | undefined> {
const dir = Instance.directory
async function resolveProjectId(dir: string): Promise<string | undefined> {
// 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<string, string | undefined>({
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),
})
}),
)
@@ -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" })
})
})
-1
View File
@@ -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" }
}
},