From a7dc1ea28445555d5944066936fdf6e1b21d27ea Mon Sep 17 00:00:00 2001 From: bj456736 <161294296+bj456736@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:46:52 +0800 Subject: [PATCH] fix(agent-core-v2): degrade media tool registration when the bound model alias is stale (#2985) * 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. * 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. --------- Co-authored-by: Mira --- .changeset/media-registrar-stale-alias.md | 5 +++ .../src/agent/media/mediaToolsRegistrar.ts | 17 +++++++- .../test/agent/media/tools/read-media.test.ts | 43 ++++++++++++++++--- 3 files changed, 58 insertions(+), 7 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 000000000..692785e5b --- /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 15f7df2db..90818fa8d 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 @@ -125,8 +133,13 @@ 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; + 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 7f256580f..14ddd1575 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,13 +9,17 @@ 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'; 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'; @@ -873,10 +877,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', @@ -916,7 +924,13 @@ describe('AgentMediaToolsRegistrar', () => { runtimeAvailable = available; runtimeChanges.fire(); }; - return { registry, registrar, bindModel, setRuntimeAvailable }; + const breakAlias = (alias: string): void => { + brokenAliases.add(alias); + }; + 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', () => { @@ -970,6 +984,25 @@ describe('AgentMediaToolsRegistrar', () => { expect(registry.resolve('ReadMediaFile')).toBe(first); }); + 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, healAlias } = createRegistrarHarness(); + breakAlias('stale-model'); + 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(); + } + }); + it('unregisters on dispose', () => { const { registry, registrar, bindModel } = createRegistrarHarness(); bindModel('vision-model', capabilities({ image_in: true, video_in: true }));