Skip to content

fix(mcp): recover failed pooled connections without replaying calls - #11392

Closed
destire-mio wants to merge 12 commits into
QwenLM:mainfrom
destire-mio:codex/fix-pooled-mcp-recovery
Closed

destire-mio wants to merge 12 commits into
QwenLM:mainfrom
destire-mio:codex/fix-pooled-mcp-recovery

Conversation

@destire-mio

@destire-mio destire-mio commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Restores previously acquired MCP connections and session tool, prompt and resource registrations before later model sends or resource reads in daemon/ACP sessions and their agents. Recovery uses the existing workspace pool, current permissions and normal authentication. It performs no tool invocation or replay.

Sessions retain their subscriptions while a shared connection is restarting. Discovery, recovery and runtime additions coordinate within the session; the pool coordinates process creation and old-connection cleanup across sessions. Removal invalidates an older queued addition before it writes configuration, and queued discovery rechecks folder trust when it starts. Failed runtime replacements roll back their own tentative in-memory configuration without restoring connection authority revoked by disconnect, removal or shutdown.

Pooled ACP sessions synchronize model tool declarations and prompt commands before each model send, including recovery that produces no diagnostic. Inherited agent tools retain their source, while independent overrides retain their own connection and resource ownership. Error handling reads the owning connection: a missing transport is rejected before sending, a returned JSON-RPC business error keeps its meaning, and a lost response retains an unknown-outcome/no-replay warning.

Why it's needed

A tool invocation, its process/transport and a session's pool reference have separate lifetimes. Cancelling one invocation can leave the shared process healthy. When the transport does fail, removing the pool entry previously left active sessions without a replacement path. Recovery must restore connection ownership and declarations without treating an interrupted operation as safe to repeat.

The counted local fixture establishes these recovery gaps, not the original reporter's cancellation mechanism. Ordinary cancellation left the fixture process alive; exit and protocol corruption were controlled faults. The runtime configuration rollback concerns in-memory state, not disk settings. A transport object can remain assigned after a network error, and an active pool entry can temporarily have a disconnected client during restart; neither state alone determines an invocation's outcome.

Reviewer Test Plan

How to verify

  • With a counted stdio service behind a loopback daemon and two ACP sessions, cancel A's slow call while B has another call in flight. B must complete, A's cancelled operation must execute once, and later calls must work. Terminate the process or corrupt the transport, then send concurrent follow-up requests; require one shared replacement process.
  • Disable or disconnect a service and require it to stay disconnected. Make replacement startup fail and check visible failure/cooldown, then restore startup and verify later recovery. Reject tool permission after reconnection and require zero denied calls on the wire. Discovery timeout must produce no tool execution.
  • Hold a shared restart in its initialize handshake while another session recovers. Keep the active subscription and avoid a false recovery cooldown. Interrupt runtime replacement with disconnect or remove; check the retained configuration and later explicit discovery, not only empty connection maps. Remove an addition before it reaches the front of the queue; the removed service must never start. Revoke folder trust before queued discovery begins; no new process may start.
  • Apply a counted HTTP tool side effect and drop the response socket while the SDK transport remains assigned. Both standalone and pooled paths must report the uncertain outcome without replay; a later new call must work. A tool whose transport is already absent must report not sent with wire count zero. A JSON-RPC application error with code 404 or disconnected-service wording must retain its original meaning.
  • Check that a registry removal with no recovery notice reaches the next ACP model request's tool declarations. Check inherited agent tools/resources and same-named independent overrides remain with their owners. During old-process cleanup, a timed-out acquire must retain the cleanup barrier; a draining process must retain its budget reservation until retirement.

Evidence (Before & After)

The repair is committed as fb125a154b51b227371bdcb49c916bcb1602f9d0, based on reviewed head 82b635b0e58044a8345ccdf62447469e765559be, including the maintainer's main merge. The tested CLI entrypoint SHA256 is 1f6de4cb4679d4c47add276e433feeea657616d9d33c0d351bff720419e02ef9. Its five-file production delta from the reviewed head has SHA256 7cc812236bc68f4e0967b1a91cda669f7801764d8d64634adcd8566d5051085b.

Check Result
Relevant core tests 1,529 pass across 12 suites
ACP Session, Channel MCP IPC and SDK MCP controller tests 933 pass across 3 suites; 2,462 core + CLI tests total
Final-bundle daemon/ACP Eight fresh scenarios pass: ordinary cancellation plus independent exit, exit on cancel, protocol corruption, disable, recovery failure/cooldown, permission denial, discovery timeout and runtime management
Counted daemon outcomes Each of six cancellation scenarios records the cancelled operation once; 53 tool calls, 16 MCP processes, all processes exited
Real source integration 12 stdio scenarios and 2 HTTP modes pass; 21 calls, 21 stdio children exited, both HTTP ports closed
HTTP response loss Reviewed code returned bare fetch failed with a recorded side effect; repaired standalone and pooled paths return the outcome warning, keep the side-effect count at one and support the next new call
Queued removal and trust revocation Unchanged real stdio probes fail on the reviewed head and pass after repair; the manager source remained unchanged through final verification
Business error 404 A real stdio counterexample failed on the first repair candidate; final production preserves the application response and call count one
Default drain budget Real removal retains capacity for the default 30-second drain; a new server is admitted after retirement, and a healthy sibling retains its reservation

The full workspace baseline build, repaired core/CLI builds, bundle, repository-wide typecheck, final core typecheck, changed-file ESLint/Prettier and diff checks pass on Node 22.22.3. The CLI compiler used a 6 GiB heap. Two final source self-review passes and two independent review passes found no additional actionable defect within the examined ownership and consumer paths. These are bounded review results, separate from hosted CI and maintainer acceptance.

The post-verification built-output manifest covers 1,061 files (118,342,322 bytes), with manifest SHA256 e6e8eba23b352510b1aa1261a97407698687c18e98bf218526e7d2d28ae8fe41; the entrypoint hash above is not a substitute for the full output tree. No rebuild occurred between daemon verification and this snapshot.

The manager approval-revocation producer and the next-send ACP declaration consumer were verified separately; this is not a single end-to-end revocation experiment. Never-settling cleanup was tested by fault injection. With the installed SDK 2.0.0, a real stdio child ignoring EOF and SIGTERM exited in about four seconds; a permanent built-in-transport cleanup hang was not reproduced. Early invalid fixture parameters and a nonexistent test directory were corrected and are not counted as production failures. The bot's previously inconclusive whole-diff test-efficacy probe is not claimed as completed by these targeted checks.

Tested on

OS Status
macOS Node 22.22.3; builds, static checks, 2,462 tests, real daemon/ACP, stdio and HTTP probes
Windows Not tested locally
Linux Not tested locally; hosted CI reports separately

Environment

Loopback daemon, WebSocket ACP, deterministic local model endpoint, counted stdio and random-port HTTP fixtures, isolated test homes and runtime directories. Raw request, cancellation, process and pool-state evidence is retained locally; results above use wire counts and actual tool outcomes, not model prose or end_turn.

Risk & Scope

  • Recovery can delay a model send. Old-entry cleanup has a five-second caller deadline; timeout reports failure and retains the barrier rather than authorizing a duplicate process. The shared five-second recovery cooldown is separate from this wait and from existing discovery timeouts. Cancellation stops that caller waiting; explicit management remains outside the cooldown.
  • Per-send declaration synchronization has a refresh cost. It reuses the existing mechanism without adding a declaration cache, management platform or generic retry framework. Initial acquisition failure and explicit closure do not become demand-recovery candidates. Trust, approval, disabled state, configuration and authentication retain their existing boundaries.
  • No breaking configuration change or migration. Ownership stays in the existing workspace pool and management surface associated with feat(serve): manage MCP through workspace runtimes #7309. Standalone invocation recovery retains its existing policy. Independent agents keep diagnostics in their owning manager/debug log.
  • Production Channel infrastructure, real OAuth providers and cross-platform process semantics are not validated locally. Preventing client replay cannot guarantee an arbitrary future model will not issue a similar business operation.
  • The review's nonblocking performance, abstraction and additional-test suggestions remain follow-up work under the repository's convergence rule. The body-only same-recipe re-add finding refers to a gate already changed in the reviewed merged head; this repair preserves that behavior rather than adding another re-add path. No review thread is marked resolved by this update.

Design documents: English · 中文. Both versions cover the same decisions, constraints, validation scenarios and evidence boundaries.

Linked Issues

Related to #11272. This implements connection recovery, not the proposed in-chat named restart command.

中文说明

本 PR 的改动

在 daemon/ACP 会话及其 Agent 的后续模型发送或资源读取前,恢复曾取得的 MCP 连接与会话工具、提示词和资源注册。恢复使用现有工作区连接池、当前权限和原有认证流程,不执行工具调用、不重放旧调用。

共享连接重启期间保留会话订阅。发现、恢复和运行时添加在会话内协调;连接池协调跨会话进程创建与旧连接清理。移除会使尚未写入配置的旧排队添加失效,排队发现开始时复查目录信任。运行时替换失败回滚本次临时内存配置,但不恢复被断开、移除或停止撤销的连接权限。

每次池化 ACP 模型发送前同步模型工具声明和提示词命令,包括恢复没有诊断提示的情况。继承工具保持来源归属,独立覆盖保留自己的连接和资源。错误处理读取所属连接:transport 缺失时在发送前拒绝,收到的 JSON-RPC 业务错误保持原义,响应丢失保留结果未知与没有重放的提示。

为什么需要

工具调用、进程/传输、会话池引用具有不同生命周期。取消一个调用可以保留健康共享进程。传输失效时,旧实现清理池条目却没有给活跃会话提供获取替代连接的路径。恢复需要重建连接归属和声明,不能推断被打断的操作可以重复执行。

本地计数服务证明这些恢复缺口,不能证明原报告中的取消机制。普通取消保留了服务进程;退出和协议损坏是受控故障。运行时配置回滚处理内存状态,不修改磁盘设置。网络错误后 transport 对象可能存在;重启中的 active 池条目也可能暂时持有断开的客户端,两种状态不能独自决定调用结果。

审查者测试计划

如何验证

  • 在本机 daemon 后配置计数 stdio 服务与两个 ACP 会话。B 正在调用时取消 A 的慢调用。B 应完成,A 的取消操作应执行一次,后续调用可用。结束进程或损坏传输后并发发送请求,要求共用一个替代进程。
  • 禁用或断开服务,要求保持断开。让替代进程启动失败,检查可见失败状态与冷却;恢复启动条件后验证后续恢复。重连后拒绝工具权限,要求 wire 上没有被拒绝的调用。发现超时不能产生工具执行。
  • 将共享重启停在初始化握手期间,让另一个会话恢复。保持 active 订阅,不能误加恢复冷却。用断开或移除打断运行时替换,检查保留配置和后续显式发现,不只检查空连接 Map。添加尚未开始时移除它,被移除服务不能启动。排队发现执行前撤销目录信任,不能启动新进程。
  • 让 HTTP 工具完成有计数的副作用后断开响应 socket,保留 SDK transport 对象。standalone 和 pooled 路径应提示结果不确定、不重放,后续新调用可用。transport 缺失的工具应提示未发送,wire 计数为零。JSON-RPC 业务错误码 404 或包含下游断线文字的错误应保留原义。
  • 验证没有恢复提示时,注册表移除能够更新下一次 ACP 模型请求的工具声明。检查继承 Agent 的工具/资源与同名独立覆盖保留归属。旧进程清理期间,获取超时应保留清理屏障;draining 进程在退出前应保留预算。

前后证据

修复提交为 fb125a154b51b227371bdcb49c916bcb1602f9d0,基于评审 head 82b635b0e58044a8345ccdf62447469e765559be,包含维护者的 main 合并。被测 CLI 入口文件 SHA256 为 1f6de4cb4679d4c47add276e433feeea657616d9d33c0d351bff720419e02ef9。相对评审 head 的五文件生产差异 SHA256 为 7cc812236bc68f4e0967b1a91cda669f7801764d8d64634adcd8566d5051085b。

检查 结果
相关 core 测试 12 个套件、1,529 项通过
ACP Session、Channel MCP IPC 和 SDK MCP controller 测试 3 个套件、933 项通过;core 与 CLI 共 2,462 项
最终 bundle 的 daemon/ACP 八个新场景通过:普通取消后独立退出、取消时退出、协议损坏、禁用、恢复失败/冷却、权限拒绝、发现超时、运行时管理
daemon 调用计数 六个取消场景各记录一次取消操作;53 次工具调用、16 个 MCP 进程,进程退出
真实源集成 12 个 stdio 场景和 2 个 HTTP 模式通过;21 次调用、21 个 stdio 子进程退出、两个 HTTP 端口关闭
HTTP 响应丢失 评审代码在服务端记录副作用后返回裸 fetch failed;修复后的两种路径保留结果提示,副作用计数为一,后续新调用成功
排队删除和信任撤销 不变的真实 stdio 探针在评审 head 失败,修后通过;manager 源码在最终验收前未变化
业务错误 404 真实 stdio 反例在第一份修复候选失败;最终代码保留业务响应,调用计数为一
默认 drain 预算 真实移除在默认三十秒 drain 期间保留容量,退出后允许新服务,健康共享会话保留预算

Node 22.22.3 下,基线全工作区构建、修后 core/CLI 构建、bundle、全仓类型检查、最终 core 类型检查、变更文件 ESLint/Prettier 和 diff 检查通过。CLI 编译使用 6 GiB 堆。最终两轮源码自审和两轮独立复审,在所检查的归属及消费路径内未发现额外可处理缺陷。该结论有范围限制,区别于托管 CI 和维护者接受状态。

验收后的构建产物清单包含 1,061 个文件(118,342,322 字节),清单 SHA256 为 e6e8eba23b352510b1aa1261a97407698687c18e98bf218526e7d2d28ae8fe41;上述入口文件指纹不代替完整产物目录。daemon 验证与该快照之间没有重新构建。

审批撤销的 manager 生产端和下次发送的 ACP 声明消费端分开验证,不是一次完整的撤销端到端实验。永不完成的清理通过故障注入测试。已安装 SDK 2.0.0 的真实 stdio 进程忽略 EOF 和 SIGTERM 后约四秒退出,没有复现内置传输永久清理挂起。早期无效夹具参数与不存在的测试目录经过修正,不计入产品失败。此前 bot 未得出结论的全差异测试效能探针,不由本轮定向检查宣称完成。

测试平台

系统 状态
macOS Node 22.22.3;构建、静态检查、2,462 项测试、真实 daemon/ACP、stdio 和 HTTP 探针
Windows 未进行本地测试
Linux 未进行本地测试,托管 CI 状态另行报告

环境

本机 daemon、WebSocket ACP、确定性本地模型端点、计数 stdio 和随机端口 HTTP 夹具、隔离的测试用户目录和运行目录。原始请求、取消、进程及连接池状态证据保存在本地;上述结果依据 wire 计数和实际工具输出,不依据模型文字或 end_turn。

风险与范围

  • 恢复会延迟模型发送。旧条目清理有五秒调用者期限;超时报告失败并保留屏障,不能授权启动重复进程。共享五秒恢复冷却独立于这段等待与现有发现超时。取消结束该调用者等待,显式管理不受冷却限制。
  • 每次发送同步声明有刷新成本。复用现有机制,不新增声明缓存、管理平台或通用重试框架。首次获取失败和主动关闭不成为按需恢复候选。保留信任、审批、禁用状态、配置和认证边界。
  • 没有破坏性配置变化或迁移要求。归属保持在现有工作区池与 feat(serve): manage MCP through workspace runtimes #7309 相关管理范围。standalone 调用恢复保留现有策略。独立 Agent 的诊断留在所属 manager/调试日志。
  • 未在本地验证生产 Channel 基础设施、真实 OAuth 服务商和跨平台进程行为。客户端不重放不能保证任意模型在未来不会请求同类业务操作。
  • 按仓库收敛规则,将 review 中非阻断的性能、抽象和额外测试建议留作后续。正文中的同配置 re-add finding 对应的条件在被审查的合并 head 中已有修改;本轮保留该行为,不增加另一条 re-add 路径。本次更新不标记 review thread 为 resolved。

设计文档:English · 中文。两版覆盖相同决策、约束、验证场景与证据边界。

关联 Issue

关联 #11272。本改动实现连接恢复,不包含所提议的聊天内按名称重启命令。

@destire-mio destire-mio changed the title fix(mcp): recover failed pooled connections on subsequent ACP turns fix(mcp): recover failed pooled connections without replaying calls Sep 10, 2026

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at ea4c678. I spent most of this review trying to break the security-relevant claims in the design and could not — the parts I was most worried about are handled correctly, and I want to record that explicitly since it is the useful output here.

What I verified holds:

  • A tool that carries the bootstrap Config cannot spawn a private client. mcp-tool.ts fails closed on !this.reconnectOnError for connection errors — it throws the shared-connection message so the model learns the outcome is unknown, and shouldAttemptReconnect returns false before any client work. reconnectOnError is genuinely populated, not a dead default: session-mcp-view.ts passes false explicitly when cloning a tool for a session.
  • Guarded invocations fail closed on reconnect, with the right reasoning in place — one authorization must not become several execution attempts when a transport error leaves the side effect ambiguous.
  • The per-session tool clone does not bake in the first subscriber's trust or eager-load choice; withSessionConfig takes them per session.
  • The recovery cooldown does not have the "an earlier concurrent success clears a later failure" bug, and the expiry timer checks identity before deleting, so an older failure cannot clear a newer cooldown for the same key.

Two findings, neither Critical.

Route ownership — a process-global read drives a live-session-owner decision. isConnectionError treats getAllMCPServerStatuses().get(this.serverName) === DISCONNECTED as proof that this session's error is a connection error. That status map is keyed by server name only and is process-global, so in the daemon, one session's server going DISCONNECTED will make an unrelated error in another session's tool for a same-named server get reported to the model as "connection error, outcome unknown." Bounded and self-correcting, but it is the wrong scope for the decision — this should consult the status of the connection this tool actually holds.

Cross-session coupling in the recovery cooldown. recoveryRetryAfter is keyed by connectionIdOf(serverName, cfg), which is process-global rather than session-scoped, so a session with a locally broken environment puts every other session's recovery for that config into a 5s cooldown. Mitigated by the fact that explicit discovery/restart goes through the normal acquire path and bypasses the cooldown, which the code notes — so users have an escape hatch. Worth stating the intended scope in the design doc either way.

The main thing I want to raise is scope, and it is a maintainer call rather than a review finding.

This is roughly +1100/−270 production lines across packages/core/src/tools/**, packages/core/src/agents/**, packages/core/src/subagents/**, and packages/cli/src/acp-integration/** — core infrastructure, cross-package, and it introduces two new modules (mcp-pool-entry.ts, mcp-workspace-budget.ts) alongside the recovery change itself. Under our two-tier gate a fix of this size is not size-blocked, but it does escalate for awareness, and I cannot claim to name every downstream consumer of the pool and registry surfaces being changed here with 100% confidence. So: escalating to maintainer review, with the note that the security-critical paths check out.

Concretely, I think this would be easier to land as two changes — the pooling/budget infrastructure first, then the recovery-without-replay behavior on top. Against Simplicity First, a single PR that both restructures pool ownership and changes recovery semantics is hard to review as one unit, and the test additions (+3800 lines) suggest the same.

The design doc is present in both English and Chinese, which is appreciated.

CI: the Test (ubuntu-latest, Node 22.x) lane is failing on this SHA and the branch is BLOCKED. Please get that green before the next round — I did not attempt to attribute the failure, since I could not run tests locally (the worktree lacks node_modules/packages/core/dist, and the vitest globalSetup guard blocks runs until npm run build from the repo root). Everything above is read-verified against ea4c678.

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agent-assisted review at fb125a154b51b227371bdcb49c916bcb1602f9d0 — Partial review — coverage gaps; no additional Critical independently confirmed. This is not a C=0/all-clear conclusion.

Current-head reconciliation

  • R3-12 / R5-1: mcp-client-manager.ts:3489-3504 rolls back the tentative overlay by object identity; canRollback() now gates only restoration of recovery authority. The same-fingerprint path preserves an active entry during restart. Current lifecycle tests cover failed replacement plus remove/disconnect and queued-add invalidation; the old stale-overlay report should not be repeated against this implementation.
  • R4-2: mcp-tool.ts:563-575 reads the owning McpClient status callback before legacy name-only fallback, while separately detecting a closed SDK transport. Discovery supplies that callback and projections/clones/retry construction preserve it. This addresses both cross-connection contamination and the later onerror-with-transport-present regression.
  • R4-3: raw recipe lookup via getMcpServers(false) preserves extension object identity through Config and subagent overrides; independent overrides still compare effective transport/session metadata.
  • R5-2: ACP now synchronizes tools and commands on every pooled pre-send, even with no diagnostic notice. R5-5's definitely-not-sent direct-client case now rejects before callTool (mcp-tool.ts:624-636); pooled failures never enter standalone reconnect/replay. JSON-RPC application errors remain distinct from transport loss.
  • R5-4: cleanup is published before terminal notifications, and acquire has a five-second caller deadline (mcp-transport-pool.ts:284-305). Timeout intentionally retains the barrier; it is not evidence the old process exited. The previous unbounded-caller-wait mechanism is changed, not a reason to permit overlapping replacement processes. Selected tests verify no spawn before cleanup, timeout, and later recovery.
  • R2-2's stale-handle mechanism is addressed by expected-handle release plus registry seats; parent/child registries sharing a logical session have separate seats. R3-2's empty refusal event is guarded by pending refusal count. The Chinese design now includes the previously omitted qualifications. Shared cooldown coupling remains explicitly documented; deferred suggestions are not reopened.

Coverage: reviewed the current production changes across invocation, client close/discovery, pool entry/transport lifetime, manager recovery/runtime serialization, registry inheritance/resource ownership, budget, Config/subagents, agent pre-send, and ACP synchronization; sampled current lifecycle/cleanup tests. Existing selected-workspace routes use the selected runtime bridge; session mutations resolve the live session's Config, while legacy workspace fan-out remains a separate existing surface. No new management route is added.

Not fully cleared: the full historical thread inventory was fetched (108 threads, no nested pagination remaining), but earlier R1/R2/R3 discovery/exclusion/resource interleavings and R4-9's budget test-versus-real-pool claim were not exhaustively re-adjudicated. Nor were all changed tests, every existing daemon dispatch/auth consumer, native transport late-close/restart races, all callable-only paths, real OAuth, or Linux/Windows process cleanup. Static safeguards are not an end-to-end security or liveness proof. The roughly 1,544 changed production-source lines warrant large-fix advisory review, not the external-refactor hard block.

Validation: static pinned-source/dataflow and selected-test inspection only; no tests, builds, daemon runs, or PR code executed. Comment only; no approval implied.

@wenshao

wenshao commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

Both this branch and main inserted new Session fields at the same anchor
after `private turn`, so the declaration block conflicted. The two sets
are independent: the recovery notice fields gate per-turn MCP recovery
diagnostics, and main's nested-sequence counter names code-mode sub-calls.
Keep both rather than choosing a side.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code resolved the merge conflicts and pushed the branch update.

PR #11392 — conflict resolution

Merged origin/main (fdb33117b4); one hunk in packages/cli/src/acp-integration/session/Session.ts.

Root cause

main's #10607 (4ec9f63d0a, CodeModeOnly tool execution) inserted private codeModeNestedSequence = 0; into the Session field block at the anchor this PR's 89886e994c had claimed — right after private turn: number = 0;. Two independent insertions on one anchor line; git cannot order them. Main had moved 49 commits / 844 files, and this was the only conflict.

Textual, not semantic

Adjacent only; neither side reads the other's field. Union:

private turn: number = 0;
private mcpRecoveryNoticeTurn?: string;
private readonly mcpRecoveryNotices = new Map<string, string>();
private codeModeNestedSequence = 0;

Both are live (Session.ts:13896, 8093-8100), so neither was droppable.

Load-bearing

  • All three fields must stay declared. Order is not load-bearing (no interdependencies); do not regroup them assuming position matters.
  • Two orderings git produced silently that a later edit could break: ToolRegistry.stop() clears copiedMcpTools before draining this.inflight; agent-core.ts runs refreshMcpTools and reassigns toolsList before promptId is built, inside the per-round try. prepareTools() is now also main's CodeModeOnly entry point, so the refresh must stay upstream of it or a recovered server never reaches the exec bindings.

Not verified

No build, typecheck, or tests run.

  • CodeModeOnly × pooled recovery is covered by neither side. feat(core): add CodeModeOnly tool execution #10607 makes getFunctionDeclarations* / getDeferredToolSummary return code-mode projections; this PR's refreshMcpTools() mutates this.tools mid-session, and those mutations now feed planCodeModeBindings — so a recovery changes the exec tool's bound schemas.
  • Session.test.ts (248 PR lines) merged with main's edits to the same file (main also made #settleTurnRecording async); the suite never ran.
  • The PR widened Config.getMcpServers(includeExtensionMetadata = true): backward compatible, and main did not touch the context interface — but I did not enumerate its new callers.
中文说明

仅 Session.ts 一处冲突。

根因:main 的 #10607(4ec9f63d0a,CodeModeOnly)在 Session 字段块紧接 private turn: number = 0; 后插入 codeModeNestedSequence,正是本 PR(89886e994c)插入两个 MCP 恢复字段之处;两次无关插入同锚点,git 无法排序。main 已前进 49 提交、844 文件,此为唯一冲突。

纯文本冲突,非语义冲突:两侧仅相邻,互不读写对方字段,故取并集。两字段均在用(Session.ts:13896、8093-8100),不可丢弃任一侧。

关键约束:三字段须保留声明;顺序不重要(无相互依赖),勿以为位置有意义而重排。另两处 git 静默产出、易被后续修改破坏的顺序:ToolRegistry.stop() 须在排空 this.inflight 前清理 copiedMcpTools;agent-core.ts 须在构造 promptId 前、每轮 try 内完成 refreshMcpTools 并重赋 toolsList。prepareTools() 现亦是 CodeModeOnly 入口,刷新须在其上游,否则恢复的服务器进不了 exec 绑定。

未能验证:未跑构建、类型检查或测试。CodeModeOnly 与连接池恢复的组合两侧均无覆盖:#10607 令 getFunctionDeclarations*、getDeferredToolSummary 返回 code-mode 投影,本 PR 的 refreshMcpTools() 则会在会话中改写 this.tools,该改写现进入 planCodeModeBindings,故一次恢复会改变 exec 绑定的 schema。Session.test.ts(PR 新增 248 行)已与 main 对同文件的修改(含 #settleTurnRecording 转 async)合并,测试未跑。PR 放宽的 Config.getMcpServers(includeExtensionMetadata = true) 向后兼容、main 未改上下文接口,但未枚举其新增调用方。

@destire-mio

Copy link
Copy Markdown
Contributor Author

Splitting this per the review guidance (thank you @doudouOUC for the detailed scope analysis).

Closing the combined PR so each piece can be reviewed on its own. The reproduction/design notes remain in the thread and in the new bilingual design note under docs/design/.

destire-mio added a commit to destire-mio/qwen-code that referenced this pull request Sep 12, 2026
Infrastructure half of QwenLM#11392, split out per review guidance. Covers the
pool ownership and teardown paths that demand recovery will build on.

- PoolEntry: publish a cleanup barrier, expose waitForCleanup(), and scope
  detach/release to a handle identity so a superseded handle cannot detach
  its replacement.
- McpTransportPool: index attachments by (logical session, registry seat)
  so one session's registries release independently; track retiring entries
  and wait, under a deadline covering the teardown budget, for in-flight
  cleanup before a new process is spawned for the same fingerprint.
- McpClient: reflect an unexpected SDK close in a pool-managed client's
  status, retire the previous transport on disconnect, and bound
  transport.close()/client.close() so the cleanup barrier always settles.

Behaviour change: a pool-managed client now reports DISCONNECTED on an
unexpected transport close, which evicts the entry and removes the
session's MCP tools. Before, an EOF-killed server was invisible. This is
the detection half of QwenLM#11272; re-acquisition is a follow-up.

No recovery trigger, retry cooldown, budget refusal accounting, or
raw-recipe access is included; those land with the follow-up that calls
them.
destire-mio added a commit to destire-mio/qwen-code that referenced this pull request Sep 12, 2026
Bilingual design note for the infrastructure half of QwenLM#11392: the pool
ownership and teardown invariants, the unexpected-close behaviour change,
risks, and validation plan. English and Simplified Chinese are aligned.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants