import type { AgentSettings } from "../types"; const DEFAULT_SETTINGS: AgentSettings = { thinking: { enabled: true, effort: "medium", keep: "all", }, loop_control: { max_attempts_per_step: 3, reserved_context_size: 50000, }, background: { keep_alive_on_exit: false, }, // `[watch] enabled` — kimi-code 2.0.1 added the key; 2.0.2 flipped the // default to off, then #4015 flipped it back on, so an absent key reads // as "on". watch: { enabled: true, }, permission: { rules: [] }, hooks: [], }; function asRecord(value: unknown): Record { if (value && typeof value === "object" && !Array.isArray(value)) { return value as Record; } return {}; } function getSection(rawOther: unknown, key: string): T | undefined { const root = asRecord(rawOther); const section = root[key]; if (section === undefined || section === null) return undefined; return section as T; } export function getAgentSettings(rawOther: unknown): AgentSettings { const root = asRecord(rawOther); const sectionLoop = getSection( rawOther, "loop_control" ); const loop: AgentSettings["loop_control"] = { ...DEFAULT_SETTINGS.loop_control, ...sectionLoop, }; // kimi-code 0.33+ (v2 engine) renamed max_retries_per_step → // max_attempts_per_step; migrate legacy values so they still show and edit. if ( sectionLoop && sectionLoop.max_attempts_per_step === undefined && sectionLoop.max_retries_per_step !== undefined ) { loop.max_attempts_per_step = sectionLoop.max_retries_per_step; } const thinking = { ...DEFAULT_SETTINGS.thinking, ...getSection(rawOther, "thinking"), }; // Upstream engines only accept string off values ("off"/"none"/"no"; see // KEEP_OFF_VALUES) — a `false` from an old config fails the v2 validator // and the v1 strict parse. Normalize every legacy off value to "off" for // display; the serialization path (setAgentSettings) writes the string. if (thinking.keep !== undefined && thinking.keep !== "all") { thinking.keep = "off"; } const sectionPermission = getSection( rawOther, "permission" ); return { thinking, loop_control: loop, background: { ...DEFAULT_SETTINGS.background, ...getSection(rawOther, "background"), }, watch: { ...DEFAULT_SETTINGS.watch, ...getSection(rawOther, "watch"), }, permission: { rules: sectionPermission?.rules ?? [], // `dangerous_command_guard` stays undefined unless the config carries // an explicit boolean — upstream defaults it to ON (kimi-code 0.40.1), // so an absent key means "on". The env var // KIMI_CODE_DANGEROUS_COMMAND_GUARD (literal "true"/"false" only) // outranks this config value at kimi-code runtime. ...(typeof sectionPermission?.dangerous_command_guard === "boolean" ? { dangerous_command_guard: sectionPermission.dangerous_command_guard } : {}), }, hooks: getSection(rawOther, "hooks") ?? [], // Top-level booleans (upstream default true): keep only explicit values, // an absent key means "on" — the UI shows `?? true`. auto_session_title: typeof root.auto_session_title === "boolean" ? root.auto_session_title : undefined, repeat_breaker: typeof root.repeat_breaker === "boolean" ? root.repeat_breaker : undefined, }; } export function setAgentSettings( rawOther: unknown, patch: Partial, opts?: { legacyV1?: boolean } ): unknown { const root = { ...asRecord(rawOther) }; const current = getAgentSettings(rawOther); // Explicit guard value wins over the carried-over one; undefined keeps the // key absent (upstream default = on). const guard = patch.permission?.dangerous_command_guard ?? current.permission?.dangerous_command_guard; const permission: NonNullable = { rules: patch.permission?.rules ?? current.permission?.rules ?? [], ...(typeof guard === "boolean" ? { dangerous_command_guard: guard } : {}), }; const next: AgentSettings = { thinking: { ...current.thinking, ...patch.thinking }, loop_control: { ...current.loop_control, ...patch.loop_control }, background: { ...current.background, ...patch.background }, watch: { ...current.watch, ...patch.watch }, permission, hooks: patch.hooks ?? current.hooks ?? [], }; // `keep` is only written when the source config explicitly carries it or the // patch sets it — an absent key keeps the engine default ("all") instead of // being materialized, so a plain save does not add the key. const rawThinking = asRecord(getSection(rawOther, "thinking")); const patchHasKeep = patch.thinking !== undefined && "keep" in patch.thinking; if (!("keep" in rawThinking) && !patchHasKeep) { delete next.thinking?.keep; } // The v2 engine (default) only reads max_attempts_per_step; the legacy v1 // key is stripped on save unless KIMI_CODE_LEGACY_FLAG=1 (v1 engine compat). if (!opts?.legacyV1) { delete next.loop_control?.max_retries_per_step; } // `compaction_max_attempts` (kimi-code 0.43.0+): 0 / undefined means "leave // the key out" — the config then falls back to the upstream default (5) // instead of pinning a literal 0. if (!next.loop_control?.compaction_max_attempts) { delete next.loop_control?.compaction_max_attempts; } if (next.thinking) root.thinking = next.thinking; if (next.loop_control) root.loop_control = next.loop_control; if (next.background) root.background = next.background; if (next.watch) root.watch = next.watch; // Keep `[permission]` when it carries rules *or* an explicit // dangerous_command_guard — dropping the section would silently lose a // guard=false write (absent key = upstream default on). An empty rules // array is omitted rather than written as `rules = []`. const hasRules = !!permission.rules && permission.rules.length > 0; const hasGuard = typeof permission.dangerous_command_guard === "boolean"; if (hasRules || hasGuard) { root.permission = { ...(hasRules ? { rules: permission.rules } : {}), ...(hasGuard ? { dangerous_command_guard: permission.dangerous_command_guard } : {}), }; } else { delete root.permission; } if (next.hooks && next.hooks.length > 0) { root.hooks = next.hooks; } else { delete root.hooks; } // Top-level booleans with an upstream default of true: an explicit false // is written; true/undefined removes the key so the config tracks the // upstream default instead of pinning it. for (const key of ["auto_session_title", "repeat_breaker"] as const) { const value = patch[key] ?? current[key]; if (value === false) { root[key] = false; } else { delete root[key]; } } return root; }