mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
fix(agent-manager): preserve worktree sessions when backend listing fails (#8902)
When the CLI backend is temporarily unhealthy, session.list() for worktree directories throws and the catch returns []. The webview reconciliation then deletes those sessions from the store, making them disappear. Track failed directories and pass preserveSessionIds so the webview skips deletion for sessions whose absence is transient. Closes #7466
This commit is contained in:
@@ -195,10 +195,12 @@ export async function loadSessions(ctx: SessionRefreshContext): Promise<string |
|
||||
const sessions = await list(ctx.workspaceDirectory)
|
||||
const projectID = sessions[0]?.projectID
|
||||
const worktreeDirs = new Set(ctx.sessionDirectories.values())
|
||||
const failed = new Set<string>()
|
||||
const extra = await Promise.all(
|
||||
[...worktreeDirs].map((dir) =>
|
||||
list(dir).catch((err: unknown) => {
|
||||
console.error(`[Kilo] Failed to list sessions for ${dir}:`, err)
|
||||
failed.add(dir)
|
||||
return [] as Session[]
|
||||
}),
|
||||
),
|
||||
@@ -212,9 +214,19 @@ export async function loadSessions(ctx: SessionRefreshContext): Promise<string |
|
||||
}
|
||||
}
|
||||
|
||||
// Sessions whose worktree directories failed to list — the webview must
|
||||
// not delete these during reconciliation since the absence is transient.
|
||||
const preserve: string[] = []
|
||||
if (failed.size) {
|
||||
for (const [sid, dir] of ctx.sessionDirectories) {
|
||||
if (failed.has(dir)) preserve.push(sid)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.postMessage({
|
||||
type: "sessionsLoaded",
|
||||
sessions: sessions.map((s) => sessionToWebview(s)),
|
||||
...(preserve.length ? { preserveSessionIds: preserve } : {}),
|
||||
})
|
||||
|
||||
return projectID
|
||||
|
||||
@@ -157,6 +157,86 @@ describe("KiloProvider pending session refresh", () => {
|
||||
expect((sent[0] as { sessions: { id: string }[] }).sessions.map((s) => s.id)).toEqual(["ses_worktree"])
|
||||
})
|
||||
|
||||
it("preserves session ids when worktree directory listing fails", async () => {
|
||||
const sent: unknown[] = []
|
||||
const ctx = createContext({
|
||||
connectionState: "connected",
|
||||
sessionDirectories: new Map([
|
||||
["ses_wt1", "/worktree1"],
|
||||
["ses_wt2", "/worktree2"],
|
||||
]),
|
||||
listSessions: async (dir) => {
|
||||
if (dir === "/repo") {
|
||||
return [
|
||||
{
|
||||
id: "ses_root",
|
||||
projectID: "project",
|
||||
title: "root",
|
||||
directory: "/repo",
|
||||
time: { created: 1, updated: 1 },
|
||||
},
|
||||
] as never
|
||||
}
|
||||
if (dir === "/worktree1") throw new Error("backend not ready")
|
||||
return [
|
||||
{
|
||||
id: "ses_wt2",
|
||||
projectID: "project",
|
||||
title: "wt2",
|
||||
directory: "/worktree2",
|
||||
time: { created: 2, updated: 2 },
|
||||
},
|
||||
] as never
|
||||
},
|
||||
postMessage: (msg) => sent.push(msg),
|
||||
})
|
||||
|
||||
await loadSessions(ctx)
|
||||
|
||||
expect(sent).toHaveLength(1)
|
||||
const msg = sent[0] as { sessions: { id: string }[]; preserveSessionIds?: string[] }
|
||||
expect(msg.sessions.map((s) => s.id)).toEqual(["ses_root", "ses_wt2"])
|
||||
expect(msg.preserveSessionIds).toEqual(["ses_wt1"])
|
||||
})
|
||||
|
||||
it("omits preserveSessionIds when all directories succeed", async () => {
|
||||
const sent: unknown[] = []
|
||||
const ctx = createContext({
|
||||
connectionState: "connected",
|
||||
sessionDirectories: new Map([["ses_wt", "/worktree"]]),
|
||||
listSessions: async (dir) => {
|
||||
if (dir === "/repo") {
|
||||
return [
|
||||
{
|
||||
id: "ses_root",
|
||||
projectID: "project",
|
||||
title: "root",
|
||||
directory: "/repo",
|
||||
time: { created: 1, updated: 1 },
|
||||
},
|
||||
] as never
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: "ses_wt",
|
||||
projectID: "project",
|
||||
title: "wt",
|
||||
directory: "/worktree",
|
||||
time: { created: 2, updated: 2 },
|
||||
},
|
||||
] as never
|
||||
},
|
||||
postMessage: (msg) => sent.push(msg),
|
||||
})
|
||||
|
||||
await loadSessions(ctx)
|
||||
|
||||
expect(sent).toHaveLength(1)
|
||||
const msg = sent[0] as { sessions: { id: string }[]; preserveSessionIds?: string[] }
|
||||
expect(msg.sessions.map((s) => s.id)).toEqual(["ses_root", "ses_wt"])
|
||||
expect(msg.preserveSessionIds).toBeUndefined()
|
||||
})
|
||||
|
||||
it("flushes deferred refresh via flushPendingSessionRefresh", async () => {
|
||||
const { calls, fn } = createListSessions()
|
||||
const ctx = createContext()
|
||||
|
||||
@@ -687,7 +687,7 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
break
|
||||
|
||||
case "sessionsLoaded":
|
||||
handleSessionsLoaded(message.sessions)
|
||||
handleSessionsLoaded(message.sessions, message.preserveSessionIds)
|
||||
break
|
||||
|
||||
case "sessionUpdated":
|
||||
@@ -1120,16 +1120,20 @@ export const SessionProvider: ParentComponent = (props) => {
|
||||
setStore("todos", sessionID, items)
|
||||
}
|
||||
|
||||
function handleSessionsLoaded(loaded: SessionInfo[]) {
|
||||
function handleSessionsLoaded(loaded: SessionInfo[], preserve?: string[]) {
|
||||
const kept = preserve?.length ? new Set(preserve) : undefined
|
||||
batch(() => {
|
||||
// Reconcile: remove sessions not in the loaded list to prevent stale
|
||||
// entries from other projects accumulating in the store.
|
||||
// Sessions whose worktree directories failed to list are preserved —
|
||||
// their absence is transient, not a real deletion.
|
||||
const ids = new Set(loaded.map((s) => s.id))
|
||||
setStore(
|
||||
"sessions",
|
||||
produce((sessions) => {
|
||||
for (const id of Object.keys(sessions)) {
|
||||
if (id.startsWith("cloud:")) continue
|
||||
if (kept?.has(id)) continue
|
||||
if (!ids.has(id)) delete sessions[id]
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -580,6 +580,7 @@ export interface MessageCreatedMessage {
|
||||
export interface SessionsLoadedMessage {
|
||||
type: "sessionsLoaded"
|
||||
sessions: SessionInfo[]
|
||||
preserveSessionIds?: string[]
|
||||
}
|
||||
|
||||
export interface CloudSessionsLoadedMessage {
|
||||
|
||||
Reference in New Issue
Block a user