From 8db78c0ddacb60fa0814064f9693149725dc016f Mon Sep 17 00:00:00 2001 From: Mira Date: Mon, 17 Aug 2026 03:18:51 +0000 Subject: [PATCH 1/2] fix(agent-core-v2): degrade media tool registration when the bound model alias is stale A restored session replays its persisted profile.bind without catalog validation, so the profile can carry a model alias that no longer resolves (e.g. the managed kimi-code models were removed from config.toml on logout). AgentMediaToolsRegistrar.refresh() called modelCatalog.getRequester() unguarded on that alias; the throw escaped the agent.status.updated listener and was reported as an [unexpected] Error2 (config.invalid) on startup. Catch the resolution failure and degrade to "no model": media tools stay registered off the profile-reported capabilities, just without a model-bound video uploader, matching the tryResolveRawModel style used elsewhere in the profile service. --- .changeset/media-registrar-stale-alias.md | 5 +++ .../src/agent/media/mediaToolsRegistrar.ts | 15 ++++++-- .../test/agent/media/tools/read-media.test.ts | 36 ++++++++++++++++--- 3 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 .changeset/media-registrar-stale-alias.md diff --git a/.changeset/media-registrar-stale-alias.md b/.changeset/media-registrar-stale-alias.md new file mode 100644 index 00000000000..692785e5be7 --- /dev/null +++ b/.changeset/media-registrar-stale-alias.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Fix an `[unexpected] Error2: Model "" is not configured in config.toml` error printed on startup when a restored session references a model that is no longer configured (e.g. after logging out of the managed Kimi Code account). Media tool registration now degrades gracefully instead of throwing from the `agent.status.updated` listener. diff --git a/packages/agent-core-v2/src/agent/media/mediaToolsRegistrar.ts b/packages/agent-core-v2/src/agent/media/mediaToolsRegistrar.ts index a7fb1d13945..029e482eab2 100644 --- a/packages/agent-core-v2/src/agent/media/mediaToolsRegistrar.ts +++ b/packages/agent-core-v2/src/agent/media/mediaToolsRegistrar.ts @@ -124,8 +124,19 @@ export class AgentMediaToolsRegistrar extends Service implements IAgentMediaTool let requester: ModelRequester | undefined; let model: Model | undefined; if (modelAlias !== '') { - requester = this.modelCatalog.getRequester(modelAlias); - model = requester.model; + // The alias comes from persisted profile state, which is replayed on + // resume without catalog validation — it may no longer resolve (e.g. + // the model was removed from config.toml when the user logged out). + // A listener throw escapes into the event bus and is reported as an + // `[unexpected]` error, so degrade to "no model" instead: media tools + // stay registered, just without a model-bound video uploader. + try { + requester = this.modelCatalog.getRequester(modelAlias); + model = requester.model; + } catch { + requester = undefined; + model = undefined; + } } this.registration = registerMediaTools(this.toolRegistry, { runtime, diff --git a/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts b/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts index 78fb351df99..9b9e951a33a 100644 --- a/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts +++ b/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts @@ -16,6 +16,10 @@ import { Jimp } from 'jimp'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { Emitter } from '#/_base/event'; +import { + resetUnexpectedErrorHandler, + setUnexpectedErrorHandler, +} from '#/_base/errors/unexpectedError'; import type { IHostFileSystem } from '#/os/interface/hostFileSystem'; import type { IHostEnvironment } from '#/os/interface/hostEnvironment'; import type { IAgentRuntimeService } from '#/agent/runtimeBinding/agentRuntime'; @@ -872,10 +876,14 @@ describe('AgentMediaToolsRegistrar', () => { getModelCapabilities: () => state.capabilities, getModel: () => state.alias, } as unknown as IAgentProfileService; + const brokenAliases = new Set(); const modelCatalog = { - getRequester: (id: string) => ({ - model: { id, name: id, providerName: 'test', protocol: 'openai' }, - }), + getRequester: (id: string) => { + if (brokenAliases.has(id)) { + throw new Error(`Model "${id}" is not configured in config.toml.`); + } + return { model: { id, name: id, providerName: 'test', protocol: 'openai' } }; + }, } as unknown as IModelCatalog; const workspaceCtx = { workDir: '/workspace', @@ -914,7 +922,10 @@ describe('AgentMediaToolsRegistrar', () => { runtimeAvailable = available; runtimeChanges.fire(); }; - return { registry, registrar, bindModel, setRuntimeAvailable }; + const breakAlias = (alias: string): void => { + brokenAliases.add(alias); + }; + return { registry, registrar, bindModel, setRuntimeAvailable, breakAlias }; } it('registers nothing until a media-capable model binds, then registers ReadMediaFile', () => { @@ -968,6 +979,23 @@ describe('AgentMediaToolsRegistrar', () => { expect(registry.resolve('ReadMediaFile')).toBe(first); }); + it('degrades to no requester when the bound alias is not configured', () => { + const unexpected: unknown[] = []; + setUnexpectedErrorHandler((err) => unexpected.push(err)); + try { + const { registry, bindModel, breakAlias } = createRegistrarHarness(); + breakAlias('stale-model'); + // A stale alias restored from a pre-logout session no longer resolves in + // the catalog; the listener must not surface an [unexpected] error, and + // media tools stay registered off the profile-reported capabilities. + bindModel('stale-model', capabilities({ image_in: true, video_in: true })); + expect(unexpected).toHaveLength(0); + expect(registry.resolve('ReadMediaFile')).toBeInstanceOf(ReadMediaFileTool); + } finally { + resetUnexpectedErrorHandler(); + } + }); + it('unregisters on dispose', () => { const { registry, registrar, bindModel } = createRegistrarHarness(); bindModel('vision-model', capabilities({ image_in: true, video_in: true })); From 859d7fe5cbe19f263fc858148bb6be12f1539f53 Mon Sep 17 00:00:00 2001 From: Mira Date: Mon, 17 Aug 2026 04:02:41 +0000 Subject: [PATCH 2/2] test(agent-core-v2): reproduce the stale-alias regression with production-consistent collaborators A stale alias makes the real AgentProfileService report UNKNOWN_CAPABILITY, so the regression now binds unknown capabilities, asserts the tool stays unregistered without surfacing an [unexpected] error, and covers recovery once the alias resolves again. The rationale moves into the mediaToolsRegistrar file header per the package comment conventions. --- .../src/agent/media/mediaToolsRegistrar.ts | 14 +++++++------ .../test/agent/media/tools/read-media.test.ts | 21 ++++++++++++------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/agent-core-v2/src/agent/media/mediaToolsRegistrar.ts b/packages/agent-core-v2/src/agent/media/mediaToolsRegistrar.ts index 029e482eab2..16f7524dc9f 100644 --- a/packages/agent-core-v2/src/agent/media/mediaToolsRegistrar.ts +++ b/packages/agent-core-v2/src/agent/media/mediaToolsRegistrar.ts @@ -17,6 +17,14 @@ * converts `video_url` takes the inline fallback when no upload hook * exists. * + * The alias re-resolved on every refresh comes from persisted profile + * state that resume replays without catalog validation, so it may no + * longer resolve (e.g. its config.toml entry was removed on logout). + * A failed resolution degrades to "no model" — registration proceeds + * from the profile-reported capabilities without a model-bound video + * uploader — instead of throwing out of the event listener, where the + * escape would surface as an `[unexpected]` error. + * * The plain-data state (`registeredKey`) is registered into `agentState` * (`IAgentStateService`) and read/written through it; `registration` stays an * instance field (the live `IDisposable` tool-registration handle, not plain @@ -124,12 +132,6 @@ export class AgentMediaToolsRegistrar extends Service implements IAgentMediaTool let requester: ModelRequester | undefined; let model: Model | undefined; if (modelAlias !== '') { - // The alias comes from persisted profile state, which is replayed on - // resume without catalog validation — it may no longer resolve (e.g. - // the model was removed from config.toml when the user logged out). - // A listener throw escapes into the event bus and is reported as an - // `[unexpected]` error, so degrade to "no model" instead: media tools - // stay registered, just without a model-bound video uploader. try { requester = this.modelCatalog.getRequester(modelAlias); model = requester.model; diff --git a/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts b/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts index 9b9e951a33a..d93a89653f4 100644 --- a/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts +++ b/packages/agent-core-v2/test/agent/media/tools/read-media.test.ts @@ -9,7 +9,7 @@ import * as posixPath from 'node:path/posix'; -import type { ModelCapability } from '#/kosong/contract/capability'; +import { UNKNOWN_CAPABILITY, type ModelCapability } from '#/kosong/contract/capability'; import type { ContentPart } from '#/kosong/contract/message'; import { VideoUploadUnsupportedError } from '#/kosong/contract/errors'; import { Jimp } from 'jimp'; @@ -925,7 +925,10 @@ describe('AgentMediaToolsRegistrar', () => { const breakAlias = (alias: string): void => { brokenAliases.add(alias); }; - return { registry, registrar, bindModel, setRuntimeAvailable, breakAlias }; + const healAlias = (alias: string): void => { + brokenAliases.delete(alias); + }; + return { registry, registrar, bindModel, setRuntimeAvailable, breakAlias, healAlias }; } it('registers nothing until a media-capable model binds, then registers ReadMediaFile', () => { @@ -979,18 +982,20 @@ describe('AgentMediaToolsRegistrar', () => { expect(registry.resolve('ReadMediaFile')).toBe(first); }); - it('degrades to no requester when the bound alias is not configured', () => { + it('survives an unconfigured bound alias and recovers when it resolves again', () => { const unexpected: unknown[] = []; setUnexpectedErrorHandler((err) => unexpected.push(err)); try { - const { registry, bindModel, breakAlias } = createRegistrarHarness(); + const { registry, bindModel, breakAlias, healAlias } = createRegistrarHarness(); breakAlias('stale-model'); - // A stale alias restored from a pre-logout session no longer resolves in - // the catalog; the listener must not surface an [unexpected] error, and - // media tools stay registered off the profile-reported capabilities. - bindModel('stale-model', capabilities({ image_in: true, video_in: true })); + bindModel('stale-model', UNKNOWN_CAPABILITY); expect(unexpected).toHaveLength(0); + expect(registry.resolve('ReadMediaFile')).toBeUndefined(); + + healAlias('stale-model'); + bindModel('stale-model', capabilities({ image_in: true, video_in: true })); expect(registry.resolve('ReadMediaFile')).toBeInstanceOf(ReadMediaFileTool); + expect(unexpected).toHaveLength(0); } finally { resetUnexpectedErrorHandler(); }