Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions apps/kimi-code/src/migration/detect-pending.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { existsSync } from 'node:fs';

import {
detectMigration,
countImportedSessionsNeedingRepair,
shouldSuppressMigration,
type MigrationPlan,
} from '@moonshot-ai/migration-legacy';
Expand Down Expand Up @@ -35,16 +34,8 @@ export async function detectPendingMigration(
): Promise<MigrationPlan | null> {
const { sourceHome, targetHome } = input;
if (!existsSync(sourceHome)) return null;
// Imported sessions an older migrator left without turn-structure records
// are unfinished migration work the completion marker must not hide — a
// repair need lifts the suppression. The scan is cheap (one state.json plus
// a wire-head read per imported session) and failure-tolerant.
const sessionsNeedingRepair = await countImportedSessionsNeedingRepair(targetHome).catch(
() => 0,
);
if (
input.ignoreMarker !== true &&
sessionsNeedingRepair === 0 &&
shouldSuppressMigration({ sourceHome, targetHome })
) {
return null;
Comment on lines 38 to 41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the required CLI changeset

This changes user-visible CLI behavior by suppressing the legacy-migration prompt after completion or dismissal, but the commit includes no .changeset/*.md entry for @moonshot-ai/kimi-code; add a patch changeset so the fix is represented in the CLI release changelog.

AGENTS.md reference: AGENTS.md:L85-L86

Useful? React with 👍 / 👎.

Expand Down Expand Up @@ -73,9 +64,8 @@ export async function detectPendingMigration(
!plan.hasUserHistory &&
!plan.hasSkills &&
!plan.hasPlans &&
sessionsNeedingRepair === 0 &&
(plan.sessionScanFailures?.length ?? 0) === 0;
if (nothingToMigrate) return null;

return { ...plan, sessionsNeedingRepair };
return plan;
}
6 changes: 0 additions & 6 deletions apps/kimi-code/src/migration/migration-screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,11 +304,6 @@ export class MigrationScreenComponent extends Container implements Focusable {
chalk.hex(colors.success)(` ✓ ${sum.sessions.sessionsMigrated} sessions migrated`),
);
}
if (sum.sessions.sessionsRepaired > 0) {
lines.push(
chalk.hex(colors.success)(` ✓ ${sum.sessions.sessionsRepaired} sessions repaired`),
);
}
if (sum.plans.copied > 0) {
lines.push(chalk.hex(colors.success)(` ✓ ${sum.plans.copied} plan files copied`));
}
Expand All @@ -325,7 +320,6 @@ export class MigrationScreenComponent extends Container implements Focusable {
}
if (
sum.sessions.sessionsMigrated === 0 &&
sum.sessions.sessionsRepaired === 0 &&
sum.plans.copied === 0 &&
migratedKinds.length === 0
) {
Expand Down
2 changes: 1 addition & 1 deletion apps/kimi-code/src/migration/run-headless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ function logReport(
if (scope.sessions) {
log(
`sessions: scanned=${s.bucketsScanned} attempted=${s.sessionsAttempted} migrated=${s.sessionsMigrated}` +
` already-migrated=${s.sessionsAlreadyMigrated} repaired=${s.sessionsRepaired} skipped-empty=${s.sessionsSkippedEmpty}` +
` already-migrated=${s.sessionsAlreadyMigrated} skipped-empty=${s.sessionsSkippedEmpty}` +
` skipped-malformed=${s.sessionsSkippedMalformed} skipped-placeholder=${s.sessionsSkippedPlaceholder}` +
` failed=${s.sessionsFailed.length} conflicts=${s.sessionsConflicts.length}` +
(s.bucketsSkippedNonlocalKaos > 0 ? ` buckets-skipped-nonlocal-kaos=${s.bucketsSkippedNonlocalKaos}` : '') +
Expand Down
45 changes: 0 additions & 45 deletions apps/kimi-code/test/migration/detect-pending.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,49 +139,4 @@ describe('detectPendingMigration', () => {
await rm(skillsHome, { recursive: true, force: true });
}
});

async function seedImportedSession(wireSecondLine: string, importFormatVersion?: number): Promise<void> {
const dir = join(tgt, 'sessions', 'wd_test', 'ses_old-import', 'agents', 'main');
await mkdir(dir, { recursive: true });
await writeFile(
join(dir, 'wire.jsonl'),
'{"type":"metadata","protocol_version":"1.0","created_at":1}\n' + wireSecondLine + '\n',
);
await writeFile(
join(tgt, 'sessions', 'wd_test', 'ses_old-import', 'state.json'),
JSON.stringify({
custom: { imported_from_kimi_cli: true, import_format_version: importFormatVersion },
}),
);
}

it('lifts marker suppression when an imported session still lacks turn structure', async () => {
await writeFile(join(src, 'config.toml'), 'default_thinking = true\n', 'utf-8');
await writeFile(
join(src, '.migrated-to-kimi-code'),
JSON.stringify({ version: 1, target_path: tgt }),
'utf-8',
);
await seedImportedSession(
'{"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"x"}],"toolCalls":[]}}',
);
const plan = await detectPendingMigration({ sourceHome: src, targetHome: tgt });
expect(plan).not.toBeNull();
expect(plan?.sessionsNeedingRepair).toBe(1);
});

it('stays suppressed when imported sessions already carry the current import format', async () => {
await writeFile(join(src, 'config.toml'), 'default_thinking = true\n', 'utf-8');
await writeFile(
join(src, '.migrated-to-kimi-code'),
JSON.stringify({ version: 1, target_path: tgt }),
'utf-8',
);
await seedImportedSession(
'{"type":"turn.prompt","agentId":"main","input":[],"origin":{"kind":"user"},"time":1}',
2,
);
const plan = await detectPendingMigration({ sourceHome: src, targetHome: tgt });
expect(plan).toBeNull();
});
});
30 changes: 25 additions & 5 deletions apps/kimi-code/test/migration/migration-screen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,6 @@ function makeReport(
sessionsAttempted: 50,
sessionsMigrated: 50,
sessionsAlreadyMigrated: 0,
sessionsRepaired: 0,
sessionsSkippedPlaceholder: 0,
sessionsSkippedEmpty: 0,
sessionsSkippedMalformed: 0,
Expand Down Expand Up @@ -316,17 +315,38 @@ describe('MigrationScreenComponent — result phase', () => {
expect(out).toContain('2 kimi-cli plugins');
});

it('renders a repaired-sessions line and never claims nothing-to-migrate for repairs', () => {
it('renders nothing-needed-migrating when every counter is zero', () => {
const c = new MigrationScreenComponent({
plan: makePlan(),
sourceHome: '/x/.kimi',
targetHome: '/y/.kimi-code',
onComplete: () => {},
});
c._testShowResult(makeReport({ sessionsMigrated: 0, sessionsRepaired: 7 }));
c._testShowResult(
makeReport(
{ sessionsAttempted: 0, sessionsMigrated: 0 },
{
config: {
migrated: false,
tuiExtracted: false,
droppedProviders: [],
droppedModels: [],
droppedKeys: [],
configConflicts: [],
wroteSiblingDueToConflict: false,
wroteTuiSibling: false,
migratedHooks: 0,
droppedHooks: 0,
sourceUnreadable: false,
deviceIdCopied: false,
siblingContents: { providers: [], models: [], hooks: 0 },
},
userHistory: { copied: 0, skippedExisting: 0 },
},
),
);
const out = c.render(80).join('\n');
expect(out).toContain('7 sessions repaired');
expect(out).not.toContain('Nothing needed migrating');
expect(out).toContain('Nothing needed migrating');
});

it('renders migrated hooks in the ✓ line and dropped hooks as a warning', () => {
Expand Down
13 changes: 2 additions & 11 deletions apps/vscode/src/migration/legacy-migration.manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { isAbsolute, join, resolve, win32 } from "node:path";
import {
detectMigration,
runMigration,
countImportedSessionsNeedingRepair,
defaultPlansSourceDir,
shouldSuppressMigration,
type MigrationPlan,
Expand Down Expand Up @@ -278,12 +277,6 @@ export class LegacyMigrationManager {
const oauthLoginsRequiringRelogin: LegacyMigrationReauthItem[] = [];
const mcpOauthServersRequiringReauth: LegacyMigrationReauthItem[] = [];

// Sessions an older migrator left without turn-structure records are
// unfinished migration work; a repair need lifts marker suppression.
const sessionsNeedingRepair = await countImportedSessionsNeedingRepair(
this.targetHome,
).catch(() => 0);

for (const candidate of candidates) {
const sourceCheck = await checkSourceDirectory(candidate.sourceHome);
if (sourceCheck === "missing") continue;
Expand Down Expand Up @@ -353,7 +346,6 @@ export class LegacyMigrationManager {

if (
!ignoreMarker &&
sessionsNeedingRepair === 0 &&
shouldSuppressMigration({
sourceHome: candidate.sourceHome,
targetHome: this.targetHome,
Expand All @@ -365,7 +357,7 @@ export class LegacyMigrationManager {

pending.push({
preview,
plan: { ...plan, sessionsNeedingRepair },
plan,
});
}

Expand Down Expand Up @@ -565,8 +557,7 @@ function aggregateTotals(sources: readonly LegacyMigrationSourceResult[]): Legac
skills += summary.skills.copied;
planFiles += summary.plans.copied;
sessions += summary.sessions.sessionsMigrated;
alreadyMigratedSessions +=
summary.sessions.sessionsAlreadyMigrated + summary.sessions.sessionsRepaired;
alreadyMigratedSessions += summary.sessions.sessionsAlreadyMigrated;
skippedItems +=
summary.userHistory.skippedExisting +
summary.skills.skippedExisting +
Expand Down
1 change: 0 additions & 1 deletion packages/migration-legacy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export {
shouldSuppressMigration,
type MigrationSuppressionInput,
} from './marker.js';
export { countImportedSessionsNeedingRepair } from './sessions/repair-imported.js';
export { defaultPlansSourceDir } from './steps/plans.js';
export { runMigration, type RunMigrationInput } from './run-migration.js';
export {
Expand Down
1 change: 0 additions & 1 deletion packages/migration-legacy/src/run-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ function emptyConfigOnlySessions(): SessionsSummary {
sessionsAttempted: 0,
sessionsMigrated: 0,
sessionsAlreadyMigrated: 0,
sessionsRepaired: 0,
sessionsSkippedPlaceholder: 0,
sessionsSkippedEmpty: 0,
sessionsSkippedMalformed: 0,
Expand Down
8 changes: 2 additions & 6 deletions packages/migration-legacy/src/sessions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ export async function migrateSessionsStep(

let migrated = 0;
let alreadyMigrated = 0;
let repaired = 0;
let processedCount = 0;
for (const c of candidates) {
const result = await migrateOneSession({
Expand Down Expand Up @@ -156,7 +155,7 @@ export async function migrateSessionsStep(
reason: `session migrated but index append failed: ${String(error)}`,
});
}
} else if (result.outcome === 'already-migrated' || result.outcome === 'repaired') {
} else if (result.outcome === 'already-migrated') {
// The session dir exists from a prior run, but that run may have crashed
// before appending the index entry. `ensureSessionIndexEntry` is
// idempotent — it adds the entry only when absent — so a rerun
Expand All @@ -167,8 +166,7 @@ export async function migrateSessionsStep(
sessionDir: result.targetDir,
workDir: c.workdirPath,
});
if (result.outcome === 'repaired') repaired++;
else alreadyMigrated++;
alreadyMigrated++;
} catch (error) {
// The index entry is genuinely missing and could not be added — the
// session stays unreachable by id, so record it as failed.
Expand Down Expand Up @@ -204,7 +202,6 @@ export async function migrateSessionsStep(
sessionsAttempted: candidates.length,
sessionsMigrated: migrated,
sessionsAlreadyMigrated: alreadyMigrated,
sessionsRepaired: repaired,
sessionsSkippedPlaceholder,
sessionsSkippedEmpty,
sessionsSkippedMalformed,
Expand Down Expand Up @@ -287,7 +284,6 @@ function emptySummary(): SessionsSummary {
sessionsAttempted: 0,
sessionsMigrated: 0,
sessionsAlreadyMigrated: 0,
sessionsRepaired: 0,
sessionsSkippedPlaceholder: 0,
sessionsSkippedEmpty: 0,
sessionsSkippedMalformed: 0,
Expand Down
34 changes: 2 additions & 32 deletions packages/migration-legacy/src/sessions/migrate-one.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,11 @@ import { readMergedSessionState, type LegacySessionRef } from './source.js';
import { writeMainAgentWire } from './wire-writer.js';
import { writeSessionState } from './state-writer.js';
import { extractToolCallDisplays } from './tool-call-display.js';
import { repairImportedSessionWire } from './repair-imported.js';
import { buildSubagentTaskRecords, migrateLegacySubagents } from './subagents.js';
import { IMPORT_FORMAT_VERSION } from './turn-structure.js';

export type MigrateOneResult =
| { readonly outcome: 'migrated'; readonly targetDir: string }
| { readonly outcome: 'already-migrated'; readonly targetDir: string }
| { readonly outcome: 'repaired'; readonly targetDir: string }
| { readonly outcome: 'conflict'; readonly targetDir: string }
| { readonly outcome: 'empty' }
| { readonly outcome: 'failed'; readonly reason: string };
Expand All @@ -41,23 +38,9 @@ export async function migrateOneSession(input: MigrateOneInput): Promise<Migrate

if (existsSync(targetDir)) {
const cls = await classifyExistingTarget(targetDir);
// A dir we wrote ourselves on a previous run — idempotent re-run. Sessions
// imported before the current import format existed get repaired in place;
// a repair that cannot be applied is a real failure, not a silent skip:
// reporting success would leave the session permanently needing repair
// while every run misleadingly completes.
// A dir we wrote ourselves on a previous run — idempotent re-run.
if (cls === 'imported') {
const formatVersion = await readImportFormatVersion(targetDir);
if (formatVersion >= IMPORT_FORMAT_VERSION) {
return { outcome: 'already-migrated', targetDir };
}
const repaired = await repairImportedSessionWire(targetDir).catch(() => false);
if (repaired) return { outcome: 'repaired', targetDir };
return {
outcome: 'failed',
reason:
'imported session needs repair but its wire could not be repaired (missing, corrupt, or unwritable)',
};
return { outcome: 'already-migrated', targetDir };
}
// A real, unrelated kimi-code session occupies the path — a true conflict.
if (cls === 'foreign') {
Expand Down Expand Up @@ -231,19 +214,6 @@ async function applyOriginalMtime(targetDir: string, createdAtMs: number): Promi
}
}

async function readImportFormatVersion(targetDir: string): Promise<number> {
try {
const parsed: unknown = JSON.parse(await readFile(join(targetDir, 'state.json'), 'utf-8'));
if (typeof parsed !== 'object' || parsed === null) return 0;
const custom = (parsed as { custom?: unknown }).custom;
if (typeof custom !== 'object' || custom === null) return 0;
const version = (custom as Record<string, unknown>)['import_format_version'];
return typeof version === 'number' ? version : 0;
} catch {
return 0;
}
}

type ExistingTarget = 'imported' | 'foreign' | 'debris';

/**
Expand Down
Loading
Loading