fix: key harness resume coalescing by the full input

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.
This commit is contained in:
7Sageer 2026-07-31 15:16:49 +08:00
parent cd1cea0fd5
commit 7292ccc018
2 changed files with 42 additions and 7 deletions

View file

@ -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.');

View file

@ -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-'));