From 0e0baea9bdd48ab4f5f71dedfe0d27bf54ca02e9 Mon Sep 17 00:00:00 2001 From: Dmatut7 <2966283641@qq.com> Date: Sat, 13 Jun 2026 23:48:03 +0800 Subject: [PATCH 1/3] fix(migration-legacy): treat blank custom_title as absent when importing sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A whitespace-only `custom_title` (e.g. " ") on an imported kimi-cli session was written verbatim as the session title and flagged `isCustomTitle: true`. The length guards (`custom_title.length > 0`) only counted characters, never trimming, while the sibling `fallbackTitle` is trimmed — so a blank title slipped past and surfaced as an all-spaces, falsely-custom entry in the session picker. Trim the custom_title and treat a blank result as absent, so the title falls back to the prompt prefix (or "Imported session") and isCustomTitle is false. Non-blank titles are now also trimmed, matching fallbackTitle. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/sessions/state-writer.ts | 11 ++++++-- .../test/sessions/state-writer.test.ts | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/migration-legacy/src/sessions/state-writer.ts b/packages/migration-legacy/src/sessions/state-writer.ts index 12c849b9c..7f0d0ab4b 100644 --- a/packages/migration-legacy/src/sessions/state-writer.ts +++ b/packages/migration-legacy/src/sessions/state-writer.ts @@ -14,9 +14,14 @@ export interface StateWriteInput { export async function writeSessionState(sessionDir: string, input: StateWriteInput): Promise { await mkdir(sessionDir, { recursive: true, mode: 0o700 }); - const customTitle = input.oldState.custom_title ?? null; - const isCustomTitle = - customTitle !== null && customTitle.length > 0 && !input.oldState.title_generated; + const customTitleRaw = input.oldState.custom_title ?? null; + // Trim and treat a blank/whitespace-only custom_title as absent, mirroring + // how `fallbackTitle` is trimmed below. Otherwise an all-spaces title slips + // past the length checks, producing a blank session title falsely flagged as + // user-custom. + const customTitle = + customTitleRaw !== null && customTitleRaw.trim().length > 0 ? customTitleRaw.trim() : null; + const isCustomTitle = customTitle !== null && !input.oldState.title_generated; const fallbackTitle = input.lastUserPrompt.slice(0, 50).trim(); const candidateTitle = customTitle ?? fallbackTitle; const finalTitle = candidateTitle.length > 0 ? candidateTitle : 'Imported session'; diff --git a/packages/migration-legacy/test/sessions/state-writer.test.ts b/packages/migration-legacy/test/sessions/state-writer.test.ts index a393100d8..ce4ac4655 100644 --- a/packages/migration-legacy/test/sessions/state-writer.test.ts +++ b/packages/migration-legacy/test/sessions/state-writer.test.ts @@ -47,6 +47,34 @@ describe('writeSessionState', () => { expect(meta.isCustomTitle).toBe(false); }); + it('treats a blank/whitespace-only custom_title as absent and falls back', async () => { + await writeSessionState(dir, { + oldState: { custom_title: ' ', title_generated: false, wire_mtime: 1 }, + lastUserPrompt: 'a real prompt here', + sourcePath: '/a', + oldSessionUuid: 'u', + wireProtocolFromOld: null, + createdAtMs: 1, + }); + const meta = JSON.parse(await readFile(join(dir, 'state.json'), 'utf-8')); + expect(meta.title).toBe('a real prompt here'); + expect(meta.isCustomTitle).toBe(false); + }); + + it('trims surrounding whitespace from a custom_title', async () => { + await writeSessionState(dir, { + oldState: { custom_title: ' My chat ', title_generated: false, wire_mtime: 1 }, + lastUserPrompt: 'irrelevant', + sourcePath: '/a', + oldSessionUuid: 'u', + wireProtocolFromOld: null, + createdAtMs: 1, + }); + const meta = JSON.parse(await readFile(join(dir, 'state.json'), 'utf-8')); + expect(meta.title).toBe('My chat'); + expect(meta.isCustomTitle).toBe(true); + }); + it('uses Imported session as fallback when no title source', async () => { await writeSessionState(dir, { oldState: { wire_mtime: 1 }, From 29c8d02b61e4e2f0342b14d692506e9b85cad04c Mon Sep 17 00:00:00 2001 From: Dmatut7 <2966283641@qq.com> Date: Sat, 13 Jun 2026 23:48:03 +0800 Subject: [PATCH 2/3] fix(migration-legacy): don't classify valid-JSON non-object lines as corrupt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `analyzeContextContent` documents `corrupt` as "every non-blank line failed to parse" (disk damage / truncated write). But a line that JSON.parse accepted yet was a scalar/array (e.g. `42`, `"hi"`, `[]`) hit the object-shape `continue` before `hadParseableLine` was set, so a context of all valid-JSON non-objects was misreported as `corrupt` and surfaced as a migration failure instead of an empty/skipped session. Mark the line as parseable as soon as JSON.parse succeeds, before the shape check — matching the documented contract and the existing "at least one line parses → empty" test intent. Genuinely truncated lines still fail JSON.parse and remain `corrupt`. Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/migration-legacy/src/sessions/translator.ts | 6 +++++- .../migration-legacy/test/sessions/translator.test.ts | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/migration-legacy/src/sessions/translator.ts b/packages/migration-legacy/src/sessions/translator.ts index 499180b94..c82f9cf31 100644 --- a/packages/migration-legacy/src/sessions/translator.ts +++ b/packages/migration-legacy/src/sessions/translator.ts @@ -49,8 +49,12 @@ export function analyzeContextContent(lines: readonly string[]): ContextContent } catch { continue; } - if (typeof parsed !== 'object' || parsed === null) continue; + // A line that JSON.parse accepts has "parsed" per the corrupt contract + // above, even when it is a scalar/array rather than an object. Mark it + // before the shape check so an all-valid-JSON-but-no-object context is + // classified 'empty' (cleared session), not 'corrupt' (disk damage). hadParseableLine = true; + if (typeof parsed !== 'object' || parsed === null) continue; const role = (parsed as Record)['role']; if (typeof role === 'string' && USABLE_ROLES.has(role)) return 'real'; } diff --git a/packages/migration-legacy/test/sessions/translator.test.ts b/packages/migration-legacy/test/sessions/translator.test.ts index 785aab192..f3c4bb20f 100644 --- a/packages/migration-legacy/test/sessions/translator.test.ts +++ b/packages/migration-legacy/test/sessions/translator.test.ts @@ -170,4 +170,12 @@ describe('analyzeContextContent', () => { ]), ).toBe('empty'); }); + + it("'empty' (not 'corrupt') when lines are valid JSON scalars/arrays, not objects", () => { + // These lines parse successfully, so they are not "disk damage". Per the + // corrupt contract (every non-blank line *failed to parse*), they must be + // classified 'empty', not 'corrupt'. + expect(analyzeContextContent(['42', '"hi"', 'true'])).toBe('empty'); + expect(analyzeContextContent(['[]'])).toBe('empty'); + }); }); From 8ce41d784349ccfd02f6dc1ea1a18dd5faa36601 Mon Sep 17 00:00:00 2001 From: Dmatut7 <2966283641@qq.com> Date: Sat, 13 Jun 2026 23:49:07 +0800 Subject: [PATCH 3/3] chore(changeset): note kimi-cli import title/corrupt fixes Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/migration-import-title-and-corrupt-fixes.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/migration-import-title-and-corrupt-fixes.md diff --git a/.changeset/migration-import-title-and-corrupt-fixes.md b/.changeset/migration-import-title-and-corrupt-fixes.md new file mode 100644 index 000000000..217d7a929 --- /dev/null +++ b/.changeset/migration-import-title-and-corrupt-fixes.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix two kimi-cli session import edge cases: a blank/whitespace-only custom title is no longer imported as an all-spaces, falsely-custom session title (it falls back to the prompt prefix), and an imported `context.jsonl` whose lines are all valid JSON but not objects is now classified as empty rather than reported as a corrupt migration failure.