diff --git a/.changeset/ignore-obsolete-experiments.md b/.changeset/ignore-obsolete-experiments.md new file mode 100644 index 000000000..d0b206f7d --- /dev/null +++ b/.changeset/ignore-obsolete-experiments.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/agent-core": patch +"@moonshot-ai/kimi-code": patch +--- + +Allow obsolete experimental config entries to remain without blocking startup. diff --git a/packages/agent-core/src/config/schema.ts b/packages/agent-core/src/config/schema.ts index 11c6500c7..094239b73 100644 --- a/packages/agent-core/src/config/schema.ts +++ b/packages/agent-core/src/config/schema.ts @@ -1,7 +1,6 @@ import { HOOK_EVENT_TYPES } from '../session/hooks/types'; import { parsePattern } from '#/agent/permission/matches-rule'; import { ErrorCodes, KimiError } from '#/errors'; -import { FLAG_DEFINITIONS, type FlagId } from '#/flags/registry'; import { z } from 'zod'; export const ProviderTypeSchema = z.enum([ @@ -105,20 +104,7 @@ export const BackgroundConfigSchema = z.object({ export type BackgroundConfig = z.infer; -const ExperimentalFlagIdSet = new Set(FLAG_DEFINITIONS.map((def) => def.id)); - -export const ExperimentalConfigSchema = z - .record(z.string(), z.boolean()) - .superRefine((config, ctx) => { - for (const key of Object.keys(config)) { - if (ExperimentalFlagIdSet.has(key)) continue; - ctx.addIssue({ - code: 'custom', - path: [key], - message: `Unknown experimental feature "${key}".`, - }); - } - }) as z.ZodType>>; +export const ExperimentalConfigSchema = z.record(z.string(), z.boolean()); export type ExperimentalConfig = z.infer; diff --git a/packages/agent-core/test/config/configs.test.ts b/packages/agent-core/test/config/configs.test.ts index 551980f9d..dd3bb2eb8 100644 --- a/packages/agent-core/test/config/configs.test.ts +++ b/packages/agent-core/test/config/configs.test.ts @@ -279,18 +279,27 @@ micro_compaction = false expect(parseConfigString(text, configPath).experimental).toEqual(config.experimental); }); - it('rejects unknown experimental feature keys', () => { - expectKimiErrorCode( - () => - parseConfigString( - ` + it('accepts obsolete experimental feature keys as inert config', async () => { + const dir = makeTempDir(); + const configPath = join(dir, 'obsolete-experimental.toml'); + const toml = ` [experimental] -not_registered = true -`, - 'unknown-experimental.toml', - ), - ErrorCodes.CONFIG_INVALID, - ); +legacy_feature = true +obsolete_feature = false +removed_flag = true +`; + + const config = parseConfigString(toml, configPath); + + expect(config.experimental).toEqual({ + 'legacy_feature': true, + 'obsolete_feature': false, + 'removed_flag': true, + }); + + await writeConfigFile(configPath, config); + const text = await readFile(configPath, 'utf-8'); + expect(parseConfigString(text, configPath).experimental).toEqual(config.experimental); }); it('loads defaults for absent files and writes typed fields without dropping raw sections', async () => { diff --git a/packages/agent-core/test/flags/resolver.test.ts b/packages/agent-core/test/flags/resolver.test.ts index d6bbb698e..85cc6e1e5 100644 --- a/packages/agent-core/test/flags/resolver.test.ts +++ b/packages/agent-core/test/flags/resolver.test.ts @@ -106,6 +106,25 @@ describe('FlagResolver', () => { expect(resolver.enabled('b-off-default' as FlagId)).toBe(true); }); + it('ignores obsolete config override ids outside the registry', () => { + const resolver = new FlagResolver({}, DEFS, { + 'a-on-default': false, + 'legacy_feature': true, + } as never); + + expect(resolver.enabled('a-on-default' as FlagId)).toBe(false); + expect(resolver.enabled('legacy_feature' as FlagId)).toBe(false); + expect(resolver.snapshot()).toEqual({ + 'a-on-default': false, + 'b-off-default': false, + }); + expect(resolver.enabledIds()).toEqual([]); + expect(resolver.explainAll().map((feature) => feature.id)).toEqual([ + 'a-on-default', + 'b-off-default', + ]); + }); + it('keeps env precedence above config overrides', () => { const resolver = new FlagResolver( {