mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-19 10:02:04 +08:00
fix(cli): surface reload failures and skip in-flight instances
The request directory's reload failure now propagates so callers still see a failed reboot; sibling failures stay logged and isolated. InstanceStore.list() only returns settled instances, so a reload is no longer blocked by another project's in-flight boot.
This commit is contained in:
@@ -1,17 +1,29 @@
|
||||
import { Effect } from "effect"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
|
||||
/** Reload every loaded instance that belongs to a project. */
|
||||
export const reloadProject = (store: InstanceStore.Interface, projectID: string): Effect.Effect<void> =>
|
||||
/**
|
||||
* Reload every loaded instance that belongs to a project.
|
||||
*
|
||||
* Failures for the directory the request came from propagate so callers still
|
||||
* see a failed reboot. Failures for sibling instances are logged and skipped.
|
||||
*/
|
||||
export const reloadProject = (
|
||||
store: InstanceStore.Interface,
|
||||
projectID: string,
|
||||
requestDirectory: string,
|
||||
): Effect.Effect<void> =>
|
||||
Effect.gen(function* () {
|
||||
for (const ctx of yield* store.list()) {
|
||||
if (String(ctx.project.id) !== projectID) continue
|
||||
yield* store
|
||||
.reload({ directory: ctx.directory, worktree: ctx.worktree, project: ctx.project })
|
||||
.pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.logWarning("project instance reload failed", { directory: ctx.directory, cause }),
|
||||
),
|
||||
)
|
||||
const reload = store.reload({ directory: ctx.directory, worktree: ctx.worktree, project: ctx.project })
|
||||
if (ctx.directory === requestDirectory) {
|
||||
yield* reload
|
||||
continue
|
||||
}
|
||||
yield* reload.pipe(
|
||||
Effect.catchCause((cause) =>
|
||||
Effect.logWarning("project instance reload failed", { directory: ctx.directory, cause }),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -29,7 +29,7 @@ export const instanceReloadHandlers = HttpApiBuilder.group(InstanceHttpApi, "ins
|
||||
}),
|
||||
)
|
||||
}
|
||||
yield* reloadProject(store, String(ctx.project.id))
|
||||
yield* reloadProject(store, String(ctx.project.id), ctx.directory)
|
||||
return true
|
||||
})
|
||||
|
||||
|
||||
@@ -228,9 +228,18 @@ const layer: Layer.Layer<Service, never, Project.Service | InstanceBootstrap.Ser
|
||||
|
||||
// kilocode_change start - loaded instance contexts for project-scoped operations
|
||||
const list = (): Effect.Effect<InstanceContext[]> =>
|
||||
Effect.forEach([...cache.values()], (entry) => Deferred.await(entry.deferred).pipe(Effect.exit)).pipe(
|
||||
Effect.map((exits) => exits.filter(Exit.isSuccess).map((exit) => exit.value)),
|
||||
)
|
||||
Effect.forEach([...cache.values()], (entry) =>
|
||||
Deferred.isDone(entry.deferred).pipe(
|
||||
Effect.flatMap((done) =>
|
||||
done
|
||||
? Deferred.await(entry.deferred).pipe(
|
||||
Effect.exit,
|
||||
Effect.map((exit) => (Exit.isSuccess(exit) ? exit.value : undefined)),
|
||||
)
|
||||
: Effect.succeed(undefined),
|
||||
),
|
||||
),
|
||||
).pipe(Effect.map((contexts) => contexts.filter((ctx): ctx is InstanceContext => ctx !== undefined)))
|
||||
// kilocode_change end
|
||||
|
||||
yield* Effect.addFinalizer(() => disposeAll().pipe(Effect.ignore))
|
||||
|
||||
@@ -3,8 +3,9 @@ import { mkdtemp, realpath, rm } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { ProjectV2 } from "@opencode-ai/core/project"
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Exit } from "effect"
|
||||
import { reloadProject } from "@/kilocode/project/reload"
|
||||
import type { InstanceContext } from "@/project/instance-context"
|
||||
import type { Project } from "@/project/project"
|
||||
import { InstanceStore } from "@/project/instance-store"
|
||||
import { testEffect } from "../lib/effect"
|
||||
@@ -24,6 +25,15 @@ const makeProject = (id: string, worktree: string): Project.Info => ({
|
||||
time: { created: 0, updated: 0 },
|
||||
})
|
||||
|
||||
const context = (dir: string, id: string): InstanceContext => ({
|
||||
directory: dir,
|
||||
worktree: dir,
|
||||
project: makeProject(id, dir),
|
||||
})
|
||||
|
||||
const stub = (contexts: InstanceContext[], reload: InstanceStore.Interface["reload"]) =>
|
||||
({ list: () => Effect.succeed(contexts), reload }) as unknown as InstanceStore.Interface
|
||||
|
||||
describe("reloadProject", () => {
|
||||
it.live("reloads every loaded instance of the project and leaves other projects alone", () =>
|
||||
Effect.gen(function* () {
|
||||
@@ -36,7 +46,7 @@ describe("reloadProject", () => {
|
||||
const b = yield* store.load({ directory: dirB, worktree: dirB, project: makeProject("proj_reload_shared", dirB) })
|
||||
const c = yield* store.load({ directory: dirC, worktree: dirC, project: makeProject("proj_reload_other", dirC) })
|
||||
|
||||
yield* reloadProject(store, "proj_reload_shared")
|
||||
yield* reloadProject(store, "proj_reload_shared", dirA)
|
||||
|
||||
expect(yield* store.load({ directory: dirA })).not.toBe(a)
|
||||
expect(yield* store.load({ directory: dirB })).not.toBe(b)
|
||||
@@ -51,9 +61,39 @@ describe("reloadProject", () => {
|
||||
|
||||
const loaded = yield* store.load({ directory: dir, worktree: dir, project: makeProject("proj_reload_solo", dir) })
|
||||
|
||||
yield* reloadProject(store, "proj_reload_missing")
|
||||
yield* reloadProject(store, "proj_reload_missing", dir)
|
||||
|
||||
expect(yield* store.load({ directory: dir })).toBe(loaded)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("propagates a failure for the request directory", () =>
|
||||
Effect.gen(function* () {
|
||||
const dirA = "/tmp/reload-request-a"
|
||||
const dirB = "/tmp/reload-request-b"
|
||||
const contexts = [context(dirA, "proj_reload_stub"), context(dirB, "proj_reload_stub")]
|
||||
const store = stub(contexts, (input) =>
|
||||
input.directory === dirA ? Effect.die(new Error("request reload failed")) : Effect.succeed(contexts[1]!),
|
||||
)
|
||||
|
||||
const exit = yield* reloadProject(store, "proj_reload_stub", dirA).pipe(Effect.exit)
|
||||
|
||||
expect(Exit.isFailure(exit)).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("skips a failure for a sibling directory", () =>
|
||||
Effect.gen(function* () {
|
||||
const dirA = "/tmp/reload-sibling-a"
|
||||
const dirB = "/tmp/reload-sibling-b"
|
||||
const contexts = [context(dirA, "proj_reload_stub"), context(dirB, "proj_reload_stub")]
|
||||
const store = stub(contexts, (input) =>
|
||||
input.directory === dirB ? Effect.die(new Error("sibling reload failed")) : Effect.succeed(contexts[0]!),
|
||||
)
|
||||
|
||||
const exit = yield* reloadProject(store, "proj_reload_stub", dirA).pipe(Effect.exit)
|
||||
|
||||
expect(Exit.isSuccess(exit)).toBe(true)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user