From 7292ccc018124c85792039715f4f143e2b2daf53 Mon Sep 17 00:00:00 2001 From: 7Sageer <7sageer@djwcb.cn> Date: Fri, 31 Jul 2026 15:16:49 +0800 Subject: [PATCH] fix: key harness resume coalescing by the full input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Concurrent resumes only share a facade when their inputs match — a caller passing different dirs, replay, profile, or kaos options gets its own resume instead of having its options silently dropped. --- packages/node-sdk/src/kimi-harness.ts | 27 ++++++++++++++----- .../node-sdk/test/sdk-rpc-client-v2.test.ts | 22 +++++++++++++++ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/packages/node-sdk/src/kimi-harness.ts b/packages/node-sdk/src/kimi-harness.ts index 7c6d9a121..00ce84892 100644 --- a/packages/node-sdk/src/kimi-harness.ts +++ b/packages/node-sdk/src/kimi-harness.ts @@ -153,18 +153,21 @@ export class KimiHarness { return active; } - // Coalesce concurrent resumes of the same id onto one facade; without - // this, parallel callers each build their own Session over the shared - // engine handle, and one facade's close kills the engine handle under - // the other. - const inflight = this.resumeInflight.get(id); + // Coalesce concurrent resumes of the same id onto one facade, keyed by + // the full input so a caller with different options (dirs, replay, + // profile, kaos) never has them silently dropped; without this, + // parallel identical callers each build their own Session over the + // shared engine handle, and one facade's close kills the engine handle + // under the other. + const key = resumeCoalesceKey(id, input); + const inflight = this.resumeInflight.get(key); if (inflight !== undefined) return inflight; const run = this.doResumeSession(input, id); - this.resumeInflight.set(id, run); + this.resumeInflight.set(key, run); try { return await run; } finally { - if (this.resumeInflight.get(id) === run) this.resumeInflight.delete(id); + if (this.resumeInflight.get(key) === run) this.resumeInflight.delete(key); } } @@ -404,6 +407,16 @@ export class KimiHarness { const DEFAULT_SESSION_STARTED_UI_MODE = 'shell'; +function resumeCoalesceKey(id: string, input: ResumeSessionInput): string { + const { kaos, persistenceKaos, ...rest } = input; + return JSON.stringify({ + ...rest, + id, + kaos: kaos !== undefined, + persistenceKaos: persistenceKaos !== undefined, + }); +} + function normalizeSessionId(value: string): string { if (typeof value !== 'string') { throw new KimiError(ErrorCodes.SESSION_ID_REQUIRED, 'Session id is required.'); diff --git a/packages/node-sdk/test/sdk-rpc-client-v2.test.ts b/packages/node-sdk/test/sdk-rpc-client-v2.test.ts index d8a934a27..d80a9d59b 100644 --- a/packages/node-sdk/test/sdk-rpc-client-v2.test.ts +++ b/packages/node-sdk/test/sdk-rpc-client-v2.test.ts @@ -395,6 +395,28 @@ key = "${titleOAuthRef.key}" } }); + it('does not coalesce resumes with different options onto one facade', async () => { + const { harness } = await makeHarness(); + const workDir = await mkdtemp(join(tmpdir(), 'kimi-sdk-v2-work-')); + tempDirs.push(workDir); + + try { + const session = await harness.createSession({ id: 'ses_no_coalesce', workDir }); + await session.close(); + + const [plain, withReplay] = await Promise.all([ + harness.resumeSession({ id: 'ses_no_coalesce' }), + harness.resumeSession({ id: 'ses_no_coalesce', replayTurnLimit: 3 }), + ]); + + // Different options must not be silently dropped onto the first + // caller's facade — each gets its own resume. + expect(plain).not.toBe(withReplay); + } finally { + await harness.close(); + } + }); + it('reports the title state in listSessions as well as in the resumed summary', async () => { const { harness } = await makeHarness(); const workDir = await mkdtemp(join(tmpdir(), 'kimi-sdk-v2-work-'));