From 45ec073e6fb8bd541cccd01fde510c12d9ece117 Mon Sep 17 00:00:00 2001 From: KimiSwitch Dev Date: Sat, 29 Aug 2026 11:13:58 +0800 Subject: [PATCH] =?UTF-8?q?fix(config):=20=E9=80=82=E9=85=8D=20kimi-code?= =?UTF-8?q?=200.39.1=E2=80=94=E2=80=94thinking=20keep=20=E5=86=99=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E4=B8=B2=20off=EF=BC=88=E5=B8=83=E5=B0=94=E4=BC=9A?= =?UTF-8?q?=E6=95=B4=E8=8A=82=E4=BD=9C=E5=BA=9F=EF=BC=89=E3=80=81=E5=AD=98?= =?UTF-8?q?=E9=87=8F=E5=B8=83=E5=B0=94=E5=BD=92=E4=B8=80=E5=8C=96=EF=BC=9B?= =?UTF-8?q?loop=5Fcontrol=20=E9=BB=98=E8=AE=A4=E5=8F=AA=E5=86=99=20v2=20?= =?UTF-8?q?=E9=94=AE=EF=BC=88legacy=20flag=20=E6=89=8D=E5=8F=8C=E5=86=99?= =?UTF-8?q?=EF=BC=89=EF=BC=9Beffort=20=E7=A7=BB=E9=99=A4=20max=20=E6=A1=A3?= =?UTF-8?q?=EF=BC=9B=E5=AE=9E=E9=AA=8C=20flags=20=E8=A1=A5=20remote-contro?= =?UTF-8?q?l=E3=80=81wait=5Ffor/minidb=20=E6=A0=87=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=BC=80=E5=90=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands.rs | 15 +-- src/components/AgentSettingsPanel.tsx | 31 +++++- src/components/SubagentSettingsPage.tsx | 26 ++++- src/i18n/en.ts | 4 +- src/i18n/zh.ts | 4 +- src/lib/agent-settings.test.ts | 134 ++++++++++++++++++++++++ src/lib/agent-settings.ts | 38 ++++++- src/lib/subagent-settings.test.ts | 25 +++++ src/lib/subagent-settings.ts | 44 +++++++- src/types/index.ts | 14 ++- 10 files changed, 306 insertions(+), 29 deletions(-) create mode 100644 src/lib/agent-settings.test.ts diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index c38d54c..058bbed 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1293,21 +1293,24 @@ pub async fn kimi_oauth_poll( // Experimental feature env-var probe (Kimi Code secondary model etc.) // --------------------------------------------------------------------------- -/// Read the Kimi Code experimental-feature environment variables that are -/// currently set (non-empty) in this process's environment. The frontend -/// uses the result to mark config.toml `[experimental]` toggles as locked: -/// env vars outrank the config file in Kimi Code's flag resolution. +/// Read the Kimi Code environment variables that are currently set (non-empty) +/// in this process's environment. The experimental-feature vars are used by the +/// frontend to mark config.toml `[experimental]` toggles as locked: env vars +/// outrank the config file in Kimi Code's flag resolution. `KIMI_CODE_LEGACY_FLAG` +/// is probed too — the AgentSettingsPanel uses it to decide whether to +/// dual-write the v1 engine's loop_control keys. /// /// Truthy values per the CLI: `1` / `true` / `yes` / `on` (case-insensitive); /// the raw values are returned and the truthy check happens on the frontend. #[tauri::command] pub fn get_experimental_env_status() -> HashMap { - const VARS: [&str; 5] = [ + const VARS: [&str; 6] = [ "KIMI_CODE_EXPERIMENTAL_FLAG", "KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL", "KIMI_CODE_EXPERIMENTAL_TOOL_SELECT", - "KIMI_CODE_EXPERIMENTAL_ACP_V2", "KIMI_CODE_EXPERIMENTAL_PERSISTENCE_MINIDB_READMODEL", + "KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL", + "KIMI_CODE_LEGACY_FLAG", ]; let mut out = HashMap::new(); for name in VARS { diff --git a/src/components/AgentSettingsPanel.tsx b/src/components/AgentSettingsPanel.tsx index 0206abb..ed108ee 100644 --- a/src/components/AgentSettingsPanel.tsx +++ b/src/components/AgentSettingsPanel.tsx @@ -1,7 +1,14 @@ +import { useEffect, useState } from "react"; +import { invoke } from "@tauri-apps/api/core"; import { useTranslation } from "../i18n"; import type { TranslationKey } from "../i18n/zh"; import { getAgentSettings, setAgentSettings } from "../lib/agent-settings"; -import type { AgentSettings, Hook, PermissionRule } from "../types"; +import type { + AgentSettings, + ExperimentalEnvStatus, + Hook, + PermissionRule, +} from "../types"; import { Card, Checkbox, NumberField, Segmented } from "./ui/controls"; interface AgentSettingsPanelProps { @@ -9,12 +16,12 @@ interface AgentSettingsPanelProps { onChange: (nextRawOther: unknown) => void; } -const THINKING_LEVELS = ["low", "medium", "high", "max"] as const; +// Upstream removed the "max" effort tier (auto-migrates to "high"). +const THINKING_LEVELS = ["low", "medium", "high"] as const; const THINKING_LABELS: Record<(typeof THINKING_LEVELS)[number], TranslationKey> = { low: "thinkingLow", medium: "thinkingMedium", high: "thinkingHigh", - max: "thinkingMax", }; const PERMISSION_DECISIONS = ["allow", "deny", "ask"] as const; @@ -40,9 +47,23 @@ const COMMON_EVENTS = [ export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelProps) { const { t } = useTranslation(); const settings = getAgentSettings(rawOther); + /** + * KIMI_CODE_LEGACY_FLAG=1 keeps the v1 loop_control keys dual-written for + * v1-engine users; the default (v2) writes only max_attempts_per_step. + */ + const [legacyV1, setLegacyV1] = useState(false); + + useEffect(() => { + invoke("get_experimental_env_status") + .then((env) => { + const value = env?.KIMI_CODE_LEGACY_FLAG ?? ""; + setLegacyV1(["1", "true", "yes", "on"].includes(value.trim().toLowerCase())); + }) + .catch(() => setLegacyV1(false)); + }, []); const update = (patch: Partial) => { - onChange(setAgentSettings(rawOther, patch)); + onChange(setAgentSettings(rawOther, patch, { legacyV1 })); }; const updateThinking = (patch: Partial) => { @@ -95,7 +116,7 @@ export function AgentSettingsPanel({ rawOther, onChange }: AgentSettingsPanelPro label={t("thinkingKeep")} checked={settings.thinking?.keep === "all"} disabled={!thinkingEnabled} - onChange={(checked) => updateThinking({ keep: checked ? "all" : false })} + onChange={(checked) => updateThinking({ keep: checked ? "all" : "off" })} />

{t("thinkingContextHint")}

diff --git a/src/components/SubagentSettingsPage.tsx b/src/components/SubagentSettingsPage.tsx index 6c3486d..d13d0b4 100644 --- a/src/components/SubagentSettingsPage.tsx +++ b/src/components/SubagentSettingsPage.tsx @@ -9,6 +9,7 @@ import { forcedEnvValue, getExperimentalFlags, getSubagentModelPool, + isExperimentalFlagSet, isFlagLockedByEnv, isMasterEnvOn, removeSubagentPoolEntry, @@ -33,12 +34,12 @@ interface SubagentSettingsPageProps { onBack: () => void; } -const EFFORTS = ["low", "medium", "high", "max"] as const; +// Upstream removed the "max" effort tier (auto-migrates to "high"). +const EFFORTS = ["low", "medium", "high"] as const; const EFFORT_LABELS: Record<(typeof EFFORTS)[number], TranslationKey> = { low: "thinkingLow", medium: "thinkingMedium", high: "thinkingHigh", - max: "thinkingMax", }; const FLAG_LABELS: Record = { @@ -55,6 +56,7 @@ const FLAG_LABELS: Record { - const key = EFFORT_LABELS[e as (typeof EFFORTS)[number]]; + // Stored "max" tiers from old configs are shown as "high" (upstream + // removed the tier; it auto-migrates to "high"). + const key = EFFORT_LABELS[e === "max" ? "high" : (e as (typeof EFFORTS)[number])]; return key ? t(key) : e; }; @@ -297,15 +301,27 @@ export function SubagentSettingsPage({ const renderFlagRow = (flag: ExperimentalFlagDef) => { const labels = FLAG_LABELS[flag.id]; const locked = masterOn || isFlagLockedByEnv(env, flag); + // An explicitly written flag (true or false) wins; an absent flag falls + // back to the upstream default (`defaultEnabled`). + const explicitlySet = isExperimentalFlagSet(rawOther, flag.id); const on = masterOn ? true : isFlagLockedByEnv(env, flag) ? forcedEnvValue(env, flag) - : flags[flag.id] === true; + : explicitlySet + ? flags[flag.id] === true + : (flag.defaultEnabled ?? false); return (
-
{t(labels.name)}
+
+ {t(labels.name)} + {!locked && !explicitlySet && flag.defaultEnabled && ( + + {t("flagDefaultOn")} + + )} +
{t(labels.desc)} {flag.envVar} diff --git a/src/i18n/en.ts b/src/i18n/en.ts index f4c731b..bba7df1 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -138,7 +138,6 @@ export const enTranslations: Record = { thinkingLow: "Low", thinkingMedium: "Medium", thinkingHigh: "High", - thinkingMax: "Max", thinkingContextHint: "Thinking uses more context. Ensure the model context length and reserved size are sufficient.", loopControlSettings: "Loop Control", maxAttemptsPerStep: "Max attempts per step", @@ -476,6 +475,9 @@ export const enTranslations: Record = { flagWaitForDesc: "The agent can wait for background tasks to finish within the current turn", flagAutoSessionTitle: "Auto Session Title", flagAutoSessionTitleDesc: "Generate session titles automatically via the managed chat_title tool", + flagRemoteControl: "Remote Control", + flagRemoteControlDesc: "Enable the experimental remote control feature", + flagDefaultOn: "On by default", lockedByEnv: "Locked by env var", masterEnvOnHint: "KIMI_CODE_EXPERIMENTAL_FLAG is set: all experimental features are forced on by the env var", secondaryModelSection: "Subagent Model (Secondary Model)", diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index 5b9fce7..55b0b96 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -136,7 +136,6 @@ export const zhTranslations = { thinkingLow: "低", thinkingMedium: "中", thinkingHigh: "高", - thinkingMax: "最大", thinkingContextHint: "启用思考会占用更多上下文,请确保模型上下文长度和预留空间足够。", loopControlSettings: "循环控制", maxAttemptsPerStep: "单步最大尝试次数", @@ -471,6 +470,9 @@ export const zhTranslations = { flagWaitForDesc: "代理可在当前回合内等待后台任务完成,而无需结束回合再被唤醒", flagAutoSessionTitle: "自动会话标题", flagAutoSessionTitleDesc: "通过托管 chat_title 工具自动生成会话标题", + flagRemoteControl: "远程控制", + flagRemoteControlDesc: "启用实验性远程控制功能", + flagDefaultOn: "默认开启", lockedByEnv: "被环境变量锁定", masterEnvOnHint: "KIMI_CODE_EXPERIMENTAL_FLAG 已设置:全部实验功能被环境变量强制开启", secondaryModelSection: "子代理模型(次主力模型)", diff --git a/src/lib/agent-settings.test.ts b/src/lib/agent-settings.test.ts new file mode 100644 index 0000000..c111f03 --- /dev/null +++ b/src/lib/agent-settings.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it } from "vitest"; +import { getAgentSettings, setAgentSettings } from "./agent-settings"; + +// --------------------------------------------------------------------------- +// Helpers — drill into the written `[thinking]` / `[loop_control]` sections +// --------------------------------------------------------------------------- + +function thinkingOf(raw: unknown): Record { + const t = (raw as { thinking?: unknown })?.thinking; + return t && typeof t === "object" && !Array.isArray(t) + ? (t as Record) + : {}; +} + +function loopOf(raw: unknown): Record { + const l = (raw as { loop_control?: unknown })?.loop_control; + return l && typeof l === "object" && !Array.isArray(l) + ? (l as Record) + : {}; +} + +// --------------------------------------------------------------------------- +// Reading — legacy off values normalized to "off", "max" → "high" +// --------------------------------------------------------------------------- + +describe("getAgentSettings — thinking.keep normalization", () => { + it("normalizes a legacy boolean `false` to \"off\"", () => { + expect(getAgentSettings({ thinking: { keep: false } }).thinking?.keep).toBe( + "off" + ); + }); + + it("normalizes the other legacy off values (0, null, \"no\", \"none\") to \"off\"", () => { + for (const v of [0, null, "no", "none"]) { + expect( + getAgentSettings({ thinking: { keep: v } }).thinking?.keep + ).toBe("off"); + } + }); + + it("keeps \"all\" and \"off\" as-is on read", () => { + expect(getAgentSettings({ thinking: { keep: "all" } }).thinking?.keep).toBe( + "all" + ); + expect(getAgentSettings({ thinking: { keep: "off" } }).thinking?.keep).toBe( + "off" + ); + }); + + it("keeps the default \"all\" when the key is absent", () => { + expect(getAgentSettings({}).thinking?.keep).toBe("all"); + }); +}); + +describe("getAgentSettings — effort \"max\" read mapping", () => { + it("normalizes a stored \"max\" to \"high\" on read", () => { + expect(getAgentSettings({ thinking: { effort: "max" } }).thinking?.effort).toBe( + "high" + ); + }); +}); + +// --------------------------------------------------------------------------- +// Writing — keep: "all" round-trips, "off" is written for legacy off values, +// absent keep is not materialized +// --------------------------------------------------------------------------- + +describe("setAgentSettings — keep serialization", () => { + it("round-trips keep = \"all\"", () => { + const raw = { thinking: { keep: "all" } }; + const next = setAgentSettings(raw, { thinking: { keep: "all" } }); + expect(thinkingOf(next).keep).toBe("all"); + }); + + it("writes \"off\" (string) for a legacy keep = false, never a boolean", () => { + const next = setAgentSettings({ thinking: { keep: false } }, {}); + expect(thinkingOf(next).keep).toBe("off"); + }); + + it("does not write the keep key when it was never set (absent in raw and patch)", () => { + expect(thinkingOf(setAgentSettings({}, {}))).not.toHaveProperty("keep"); + const next = setAgentSettings( + { thinking: { enabled: true } }, + { thinking: { effort: "high" } } + ); + expect(thinkingOf(next)).not.toHaveProperty("keep"); + expect(thinkingOf(next).effort).toBe("high"); + }); + + it("writes the key when an absent raw config's keep is explicitly toggled off", () => { + const next = setAgentSettings({}, { thinking: { keep: "off" } }); + expect(thinkingOf(next).keep).toBe("off"); + }); +}); + +// --------------------------------------------------------------------------- +// loop_control — v2 default writes only max_attempts_per_step; legacyV1 opts +// dual-writes; read fallback from the v1 key is preserved +// --------------------------------------------------------------------------- + +describe("loop_control — v1/v2 key handling", () => { + const raw = { loop_control: { max_attempts_per_step: 5, max_retries_per_step: 5 } }; + + it("default (v2): a save strips the legacy max_retries_per_step key", () => { + const next = setAgentSettings(raw, { loop_control: { reserved_context_size: 60000 } }); + expect(loopOf(next).max_attempts_per_step).toBe(5); + expect(loopOf(next)).not.toHaveProperty("max_retries_per_step"); + }); + + it("default (v2): a dual-write patch ends up with only the v2 key", () => { + const next = setAgentSettings( + raw, + { loop_control: { max_attempts_per_step: 7, max_retries_per_step: 7 } } + ); + expect(loopOf(next).max_attempts_per_step).toBe(7); + expect(loopOf(next)).not.toHaveProperty("max_retries_per_step"); + }); + + it("legacyV1: both keys are written", () => { + const next = setAgentSettings( + raw, + { loop_control: { max_attempts_per_step: 7, max_retries_per_step: 7 } }, + { legacyV1: true } + ); + expect(loopOf(next).max_attempts_per_step).toBe(7); + expect(loopOf(next).max_retries_per_step).toBe(7); + }); + + it("read fallback keeps a max_retries_per_step-only config editable", () => { + const s = getAgentSettings({ loop_control: { max_retries_per_step: 4 } }); + expect(s.loop_control?.max_attempts_per_step).toBe(4); + expect(s.loop_control?.max_retries_per_step).toBe(4); + }); +}); \ No newline at end of file diff --git a/src/lib/agent-settings.ts b/src/lib/agent-settings.ts index 9da8a61..f45d40f 100644 --- a/src/lib/agent-settings.ts +++ b/src/lib/agent-settings.ts @@ -49,11 +49,24 @@ export function getAgentSettings(rawOther: unknown): AgentSettings { ) { 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"; + } + // The "max" effort tier was removed upstream (auto-migrates to "high"); + // old configs still carrying it are shown as "high" (not rewritten on read). + if (thinking.effort === "max") { + thinking.effort = "high"; + } return { - thinking: { - ...DEFAULT_SETTINGS.thinking, - ...getSection(rawOther, "thinking"), - }, + thinking, loop_control: loop, background: { ...DEFAULT_SETTINGS.background, @@ -70,7 +83,8 @@ export function getAgentSettings(rawOther: unknown): AgentSettings { export function setAgentSettings( rawOther: unknown, - patch: Partial + patch: Partial, + opts?: { legacyV1?: boolean } ): unknown { const root = { ...asRecord(rawOther) }; const current = getAgentSettings(rawOther); @@ -84,6 +98,20 @@ export function setAgentSettings( 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; + } if (next.thinking) root.thinking = next.thinking; if (next.loop_control) root.loop_control = next.loop_control; if (next.background) root.background = next.background; diff --git a/src/lib/subagent-settings.test.ts b/src/lib/subagent-settings.test.ts index cddb7ac..bbdd94b 100644 --- a/src/lib/subagent-settings.test.ts +++ b/src/lib/subagent-settings.test.ts @@ -231,6 +231,31 @@ describe("pool mutations", () => { expect(section(updated).default_model).toBe("kimi-k1"); }); + it("upsertSubagentPoolEntry materializes the implicit default into a new table", () => { + // Implicit single-entry form (default_model set, no models table): adding a + // second alias must pull the default into the table, otherwise the result + // violates "default_model must be a pool key" (rule 3) and gets rejected. + const raw = rawWith({ default_model: "kimi-k1", force: false }); + const added = upsertSubagentPoolEntry(raw, "kimi-k2", ""); + expect(modelsOf(added)).toEqual({ "kimi-k1": "", "kimi-k2": "" }); + expect(section(added).default_model).toBe("kimi-k1"); + const pool = getSubagentModelPool(added); + expect(pool).toBeDefined(); + expect( + validateSubagentPool(pool!, ["kimi-k1", "kimi-k2"]), + ).toEqual([]); + }); + + it("upsertSubagentPoolEntry upgrades a legacy `model`-only section to a valid pool", () => { + const raw = rawWith({ model: "kimi-k1" }); + const added = upsertSubagentPoolEntry(raw, "kimi-k2", "K2"); + expect(modelsOf(added)).toEqual({ "kimi-k1": "", "kimi-k2": "K2" }); + expect(section(added).default_model).toBe("kimi-k1"); + expect(section(added).model).toBe("kimi-k1"); + const pool = getSubagentModelPool(added); + expect(validateSubagentPool(pool!, ["kimi-k1", "kimi-k2"])).toEqual([]); + }); + it("removeSubagentPoolEntry keeps the default when removing a non-default alias", () => { const raw = rawWith({ default_model: "kimi-k1", diff --git a/src/lib/subagent-settings.ts b/src/lib/subagent-settings.ts index 3392b65..f20ce88 100644 --- a/src/lib/subagent-settings.ts +++ b/src/lib/subagent-settings.ts @@ -28,27 +28,36 @@ export interface ExperimentalFlagDef { id: string; /** Single-feature env var that can force the flag (locks the UI toggle). */ envVar: string; + /** Feature turned on by default upstream; an absent config key falls back + * to this, while an explicit `false` in `[experimental]` still wins. */ + defaultEnabled?: boolean; } /** Known experimental flags — mirrors the kimi-code v2 flag registry * (the per-feature flag.ts files under packages/agent-core-v2/src: * secondary-model, tool-select, persistence_minidb_readmodel, tower, - * subagent_fork, wait_for, auto_session_title). `acp-v2` was removed - * upstream and is dropped here. */ + * subagent_fork, wait_for, auto_session_title, remote-control). `acp-v2` was + * removed upstream and is dropped here. */ export const EXPERIMENTAL_FLAGS: ExperimentalFlagDef[] = [ { id: "secondary-model", envVar: "KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL" }, { id: "tool-select", envVar: "KIMI_CODE_EXPERIMENTAL_TOOL_SELECT" }, { id: "persistence_minidb_readmodel", envVar: "KIMI_CODE_EXPERIMENTAL_PERSISTENCE_MINIDB_READMODEL", + defaultEnabled: true, }, { id: "tower", envVar: "KIMI_CODE_EXPERIMENTAL_TOWER" }, { id: "subagent_fork", envVar: "KIMI_CODE_EXPERIMENTAL_SUBAGENT_FORK" }, - { id: "wait_for", envVar: "KIMI_CODE_EXPERIMENTAL_WAIT_FOR" }, + { + id: "wait_for", + envVar: "KIMI_CODE_EXPERIMENTAL_WAIT_FOR", + defaultEnabled: true, + }, { id: "auto_session_title", envVar: "KIMI_CODE_EXPERIMENTAL_AUTO_SESSION_TITLE", }, + { id: "remote-control", envVar: "KIMI_CODE_EXPERIMENTAL_REMOTE_CONTROL" }, ]; export const EXPERIMENTAL_MASTER_ENV = "KIMI_CODE_EXPERIMENTAL_FLAG"; @@ -87,6 +96,15 @@ export function getExperimentalFlags(rawOther: unknown): Record return out; } +/** Whether a flag is explicitly written in `[experimental]` (true *or* false). + * An absent flag falls back to the upstream default (`defaultEnabled`). */ +export function isExperimentalFlagSet(rawOther: unknown, id: string): boolean { + return Object.prototype.hasOwnProperty.call( + asRecord(asRecord(rawOther)["experimental"]), + id + ); +} + /** Set one flag in `[experimental]`. Removing all flags drops the section. */ export function setExperimentalFlag( rawOther: unknown, @@ -256,8 +274,11 @@ export function setSubagentModelPool( return root; } -/** Add or update one alias in the pool `models` table. The rest of the - * section (default, force, unknown fields) is preserved untouched. */ +/** Add or update one alias in the pool `models` table. When the section is + * the implicit single-entry form (a `default_model`/`model` but no table), + * the effective default is materialized into the table first — otherwise the + * new table would fail the engine rule "default_model must be a pool key". + * The rest of the section (force, unknown fields) is preserved untouched. */ export function upsertSubagentPoolEntry( rawOther: unknown, alias: string, @@ -269,8 +290,21 @@ export function upsertSubagentPoolEntry( for (const [k, v] of Object.entries(section)) next[k] = v; const models: Record = {}; for (const [k, v] of Object.entries(asRecord(next.models))) models[k] = v; + const effectiveDefault = + typeof next.default_model === "string" && next.default_model !== "" + ? next.default_model + : typeof next.model === "string" && next.model !== "" + ? next.model + : undefined; + if (effectiveDefault !== undefined && !(effectiveDefault in models)) { + models[effectiveDefault] = ""; + } models[alias] = description; next.models = models; + if (effectiveDefault !== undefined) { + next.default_model = effectiveDefault; + next.model = effectiveDefault; + } root.secondary_model = next; return root; } diff --git a/src/types/index.ts b/src/types/index.ts index c0b9040..3d3996f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -100,7 +100,18 @@ export interface DiscoveredModel { export interface ThinkingConfig { enabled?: boolean; + /** + * Effort tier. `max` is read-compatible only — upstream removed the tier + * (old configs auto-migrate to `high`); the UI normalizes it and the + * serialization path never writes it. + */ effort?: "low" | "medium" | "high" | "max"; + /** + * Keep thinking content. The legacy off values (`false`, `0`, "no", "none", + * `null`) are read-compatible — old configs may carry them; the UI + * normalizes them to `"off"` and the serialization path never emits a + * boolean (upstream engines only accept string off values). + */ keep?: "all" | false | 0 | "no" | "off" | "none" | null; } @@ -109,7 +120,8 @@ export interface LoopControlConfig { max_attempts_per_step?: number; /** v2 engine key (kimi-code 0.33+): `max_steps_per_run` was renamed. */ max_steps_per_turn?: number; - /** Legacy v1 key — written in sync so KIMI_CODE_LEGACY_FLAG=1 still works. */ + /** Legacy v1 key — read fallback only; written/kept only when + * KIMI_CODE_LEGACY_FLAG=1 (v1 engine compat). v2 saves strip it. */ max_retries_per_step?: number; reserved_context_size?: number; }