# Agent Note: TUI 模型上下文解析在适配器注册竞争时延后重试

Status: implemented

[English](2026-07-30-tui-adapter-registration-race.md) | 中文

## Problem

Cordis 按服务可用性而非配置顺序激活插件，因此 TUI（其 `inject` 只要求 `llm` 服务）可能在 `dsh-llm-pi-ai` 这类已配置的适配器插件完成提供方路由注册之前就挂载。TUI 的模型控制器在挂载时立即解析所选模型的上下文窗口；当 agent 的路由指向尚未注册的提供方时，`resolveModelInfo` 以 `NO_ADAPTER` 拒绝，于是每个新会话都会打印 `Could not resolve model context: no adapter registered for provider "…"` —— 对一份完全正常的配置报出的虚假错误（适配器几毫秒后就完成注册，对话也一切正常）。

## Decision

TUI 模型控制器把上下文窗口解析中的 `NO_ADAPTER` 拒绝视为瞬态状态而非错误：静默搁置这次解析，并在下一次 `llm/adapters-updated` 提交时重新解析——这是 `LlmService` 本就在每个路由提交点发出的无载荷注册表通知。若某次提交仍缺少该路由，等待会被再次搁置，因此无关的拓扑变化保持沉默。任何目标变更都会重新进入解析并清除挂起的等待，因此延后状态绝不会相对当前选择变陈旧；其他所有解析错误仍照常打印通知。

## Alternatives considered

**让 TUI 等启动结算后再解析。** TUI 不依赖 Loader（测试和嵌入方在没有 Loader 的环境下运行），而且"已结算"在插件内部不可观测；为一次外观性的解析引入 Loader 耦合会颠倒依赖方向。

**用定时器轮询或重试。** 定时器只能猜测激活延迟，遇到慢适配器仍会误报，还会引入一个没有归属者的可调参数。注册表本就通过 `llm/adapters-updated` 公告每次提交；订阅它既精确又零成本。

**调整配置顺序让适配器先加载。** Loader 中行顺序不承载加载语义（激活按设计由服务驱动），因此这无法用配置表达。

**彻底压制 NO_ADAPTER 错误。** 那样的话，永久缺失的适配器（提供方名字拼错）在上下文窗口路径上就永远不会暴露。延后重试保留了信号：错误的提供方名字仍会在选择器中表现出类似 `model unset` 的行为，并在分派时大声失败，而启动竞争则自行化解。

**改为在每次提交消息时解析上下文窗口，而不是在挂载时。** 发送路径本就按步解析（`prepareCall()`），且指示器是持续显示的，不只在发送时；按提交解析显示值会让指示器在首条消息之前一直空白，并为一个仅在路由变化时才变的值反复执行适配器 I/O。

## Consequences

真正配置错误的提供方不再在启动时打印上下文解析错误——它改在首次分派时暴露，那才是该失败可以被处理的地方。控制器订阅每次 `llm/adapters-updated` 提交，但只在有等待被搁置时才动作；监听器的 disposer 经由控制器的 `detach()` 在频道的 `detachListeners()` 中释放，与同级频道监听器保持对称。由三个 TUI 测试覆盖：延后的解析在无关提交中保持沉默、在该路由的提交到来时完成；目标变更丢弃陈旧等待；频道 detach 之后注册表提交不再重新进入解析。
