fix: skip legacy max step limit during migration (#772)

* fix: skip legacy max step limit during migration

* fix: skip more obsolete legacy migration fields
This commit is contained in:
Kai 2026-06-15 18:02:04 +08:00 committed by GitHub
parent ecd7a0afb6
commit d47e699015
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 106 additions and 0 deletions

View file

@ -0,0 +1,6 @@
---
"@moonshot-ai/kimi-code": patch
"@moonshot-ai/migration-legacy": patch
---
Do not carry obsolete legacy loop, background, plan, yolo, or unknown experimental flags into migrated config files.

View file

@ -7,6 +7,7 @@ import {
ProviderConfigSchema,
transformTomlData,
} from '@moonshot-ai/agent-core';
import { FLAG_DEFINITIONS } from '@moonshot-ai/agent-core/flags/registry';
import { atomicWrite } from '../atomic-write.js';
import { DEFAULT_CONFIG_FILE_TEXT, isTuiStubOrMissing } from '../stub-detect.js';
import {
@ -19,6 +20,18 @@ import {
// `theme` / `default_editor` belong in tui.toml, not config.toml.
const TUI_TOP_LEVEL_KEYS = new Set(['theme', 'default_editor']);
const TOP_LEVEL_KEYS_TO_DROP = new Set(['plan_mode', 'yolo']);
const LOOP_CONTROL_FIELDS_TO_KEEP = new Set([
'max_retries_per_step',
'reserved_context_size',
]);
const BACKGROUND_FIELDS_TO_KEEP = new Set([
'max_running_tasks',
'keep_alive_on_exit',
]);
const REGISTERED_EXPERIMENTAL_FLAGS: ReadonlySet<string> = new Set(
FLAG_DEFINITIONS.map((definition) => definition.id),
);
// kimi-code's tui.toml `theme` enum (mirrors apps/kimi-code TuiThemeSchema).
// A legacy theme outside this set would fail loadTuiConfig()'s whole-file
@ -97,6 +110,23 @@ function emptyResult(): ConfigStepResult {
};
}
function filterFields(
value: Record<string, unknown>,
fieldsToKeep: ReadonlySet<string>,
): Record<string, unknown> | undefined {
const keptEntries = Object.entries(value).filter(([field]) => fieldsToKeep.has(field));
return keptEntries.length > 0 ? Object.fromEntries(keptEntries) : undefined;
}
function filterRegisteredExperimentalFlags(
value: Record<string, unknown>,
): Record<string, unknown> | undefined {
const keptEntries = Object.entries(value).filter(
([field, flag]) => REGISTERED_EXPERIMENTAL_FLAGS.has(field) && typeof flag === 'boolean',
);
return keptEntries.length > 0 ? Object.fromEntries(keptEntries) : undefined;
}
/** True when the kimi-cli provider entry validates against kimi-code's schema. */
function providerIsSupported(prov: Record<string, unknown>): boolean {
const transformed = transformTomlData({ providers: { x: prov } });
@ -309,6 +339,7 @@ export async function migrateConfigStep(input: ConfigStepInput): Promise<ConfigS
for (const [k, v] of Object.entries(parsed)) {
if (k === 'providers' || k === 'models' || k === 'hooks') continue;
if (TUI_TOP_LEVEL_KEYS.has(k)) continue;
if (TOP_LEVEL_KEYS_TO_DROP.has(k)) continue;
if (k === 'default_yolo') {
// kimi-cli's `default_yolo` maps to kimi-code's `default_permission_mode`.
if (v === true) migratedTop['default_permission_mode'] = 'yolo';
@ -330,6 +361,27 @@ export async function migrateConfigStep(input: ConfigStepInput): Promise<ConfigS
) {
continue;
}
if (k === 'loop_control' && isRecord(v)) {
const filteredLoopControl = filterFields(v, LOOP_CONTROL_FIELDS_TO_KEEP);
if (filteredLoopControl !== undefined) {
migratedTop[k] = filteredLoopControl;
}
continue;
}
if (k === 'background' && isRecord(v)) {
const filteredBackground = filterFields(v, BACKGROUND_FIELDS_TO_KEEP);
if (filteredBackground !== undefined) {
migratedTop[k] = filteredBackground;
}
continue;
}
if (k === 'experimental' && isRecord(v)) {
const filteredExperimental = filterRegisteredExperimentalFlags(v);
if (filteredExperimental !== undefined) {
migratedTop[k] = filteredExperimental;
}
continue;
}
migratedTop[k] = v;
}
if (Object.keys(keptProviders).length > 0) migratedTop['providers'] = keptProviders;

View file

@ -430,6 +430,54 @@ base_url = "https://target.example/v1"
expect(r.siblingContents.hooks).toBe(2);
});
it('drops legacy migration fields but keeps supported loop and background fields', async () => {
await writeFile(
join(src, 'config.toml'),
'default_thinking = true\n' +
'plan_mode = true\n' +
'yolo = true\n' +
'[experimental]\n' +
'micro_compaction = false\n' +
'unknown_flag = true\n' +
'[loop_control]\n' +
'max_steps_per_turn = 1000\n' +
'max_steps_per_run = 42\n' +
'max_retries_per_step = 2\n' +
'max_ralph_iterations = 3\n' +
'reserved_context_size = 60000\n' +
'compaction_trigger_ratio = 0.7\n' +
'[background]\n' +
'max_running_tasks = 8\n' +
'keep_alive_on_exit = true\n' +
'kill_grace_period_ms = 2000\n' +
'print_wait_ceiling_s = 3600\n' +
'read_max_bytes = 30000\n',
);
const r = await migrateConfigStep({ sourceHome: src, targetHome: tgt });
expect(r.migrated).toBe(true);
const cfg = await readFile(join(tgt, 'config.toml'), 'utf-8');
expect(cfg).toContain('[experimental]');
expect(cfg).toContain('micro_compaction = false');
expect(cfg).not.toContain('unknown_flag');
expect(cfg).toContain('[loop_control]');
expect(cfg).toContain('max_retries_per_step = 2');
expect(cfg).toContain('reserved_context_size = 60000');
expect(cfg).not.toContain('max_steps_per_turn');
expect(cfg).not.toContain('max_steps_per_run');
expect(cfg).not.toContain('max_ralph_iterations');
expect(cfg).not.toContain('compaction_trigger_ratio');
expect(cfg).toContain('[background]');
expect(cfg).toContain('max_running_tasks = 8');
expect(cfg).toContain('keep_alive_on_exit = true');
expect(cfg).not.toContain('kill_grace_period_ms');
expect(cfg).not.toContain('print_wait_ceiling_s');
expect(cfg).not.toContain('read_max_bytes');
expect(cfg).not.toContain('plan_mode = true');
expect(cfg).not.toContain('yolo = true');
});
it('maps default_yolo to default_permission_mode = "yolo"', async () => {
await writeFile(
join(src, 'config.toml'),