From ceaa3e674e7c52129f00f2eac86e1aea24315c29 Mon Sep 17 00:00:00 2001 From: "haozhe.yang" Date: Thu, 20 Aug 2026 21:19:03 +0800 Subject: [PATCH] fix(agent-core-v2): let session archive proceed after a failed resume A failed resume is cached by SessionManager and rethrown from whenResumeSettled, so archiving a session whose workspace is gone failed with the stale resume error even though cold archive only rewrites session metadata. Swallow the settle failure: still wait for an in-flight resume before the live/cold classification, but fall through to the cold metadata path after a failed one. --- .changeset/archive-missing-workspace.md | 5 ++++ .../sessionLifecycle/coldSessionArchive.ts | 2 +- .../coldSessionArchive.test.ts | 10 +++---- packages/kap-server/test/sessions.test.ts | 26 +++++++++++++++++++ 4 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 .changeset/archive-missing-workspace.md diff --git a/.changeset/archive-missing-workspace.md b/.changeset/archive-missing-workspace.md new file mode 100644 index 0000000000..0beac2e42b --- /dev/null +++ b/.changeset/archive-missing-workspace.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix sessions failing to archive when their workspace folder no longer exists. diff --git a/packages/agent-core-v2/src/workspace/sessionLifecycle/coldSessionArchive.ts b/packages/agent-core-v2/src/workspace/sessionLifecycle/coldSessionArchive.ts index 461655f512..795709b18f 100644 --- a/packages/agent-core-v2/src/workspace/sessionLifecycle/coldSessionArchive.ts +++ b/packages/agent-core-v2/src/workspace/sessionLifecycle/coldSessionArchive.ts @@ -72,7 +72,7 @@ export async function setSessionArchived( ): Promise { const manager = accessor.get(ISessionManager); return manager.withLifecycleSerialization(sessionId, async (unguarded) => { - await manager.whenResumeSettled(sessionId); + await manager.whenResumeSettled(sessionId).catch(() => undefined); const live = getLiveSessionById(accessor, sessionId); if (live !== undefined) { if (archived) await unguarded.archive(); diff --git a/packages/agent-core-v2/test/workspace/sessionLifecycle/coldSessionArchive.test.ts b/packages/agent-core-v2/test/workspace/sessionLifecycle/coldSessionArchive.test.ts index b8467aa3ec..af3b80aac1 100644 --- a/packages/agent-core-v2/test/workspace/sessionLifecycle/coldSessionArchive.test.ts +++ b/packages/agent-core-v2/test/workspace/sessionLifecycle/coldSessionArchive.test.ts @@ -171,20 +171,16 @@ describe('setSessionArchivedBatch', () => { expect(persisted['isCustomTitle']).toBe(true); }); - it('fails the item when a concurrent resume failed instead of cold-classifying', async () => { + it('cold-classifies the item when a concurrent resume failed', async () => { const outcomes = await setSessionArchivedBatch( coldPathAccessor({ - storeGet: async () => { - throw new Error('unreachable — the settle throws first'); - }, + storeGet: async () => ({ id: 's1', createdAt: 1, updatedAt: 2, archived: false }), resumeError: new Error('resume boom'), }), ['s1'], true, ); - expect(outcomes).toEqual([ - { id: 's1', ok: false, reason: 'error', message: 'resume boom' }, - ]); + expect(outcomes).toEqual([{ id: 's1', ok: true }]); }); it('reads and migrates the legacy session-meta location before answering not_found', async () => { diff --git a/packages/kap-server/test/sessions.test.ts b/packages/kap-server/test/sessions.test.ts index b7c5b214b7..0728ae8ce3 100644 --- a/packages/kap-server/test/sessions.test.ts +++ b/packages/kap-server/test/sessions.test.ts @@ -20,9 +20,11 @@ import { IEventService, ISessionCronService, ISessionManager, + IWorkspaceService, MAIN_AGENT_ID, closeSessionById, getLiveSessionById, + resumeSessionById, sessionDirOf, type ServiceIdentifier, type ScopeSeed, @@ -782,6 +784,30 @@ describe('server-v2 /api/v1/sessions', () => { expect(got.body.data.archived).toBe(true); }); + it('archives a cold session after a failed resume when the workspace root is gone', async () => { + const cwd = join(home as string, 'gone-ws'); + await mkdir(cwd); + const created = await postJson('/api/v1/sessions', { metadata: { cwd } }); + const id = created.body.data.id; + await closeSessionById((server as RunningServer).core.accessor, id); + await (server as RunningServer).core.accessor + .get(IWorkspaceService) + .delete(encodeWorkDirKey(cwd)); + await rm(cwd, { recursive: true, force: true }); + + await expect( + resumeSessionById((server as RunningServer).core.accessor, id), + ).rejects.toThrow(/does not exist/); + + const archived = await postJson<{ archived: boolean }>(`/api/v1/sessions/${id}:archive`); + expect(archived.body.code).toBe(0); + expect(archived.body.data).toEqual({ archived: true }); + + const got = await getJson(`/api/v1/sessions/${id}`); + expect(got.body.code).toBe(0); + expect(got.body.data.archived).toBe(true); + }); + it('restores an archived session via :restore and returns it to the default list', async () => { const cwd = home as string; const created = await postJson('/api/v1/sessions', { metadata: { cwd } });