diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c0844e..3d0fbf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.13.2 + +- Exit the production stdio entry on EOF/closed pipes, unify shutdown events and terminate on process-level fatal errors. Connect transport before initialization so disconnect can cancel startup. +- Cancel startup/workspace rebinding and all MCP request signals on shutdown; prevent late resources from restarting the runtime. +- Share the existing eight-second shutdown budget across owners, retain failures and attempt independent owned-process cleanup even when an adapter hangs. +- Add actual dist-entry EOF tests and lifecycle regression coverage. Abnormal termination and idle-resource reclamation continue in the next iteration. +- Synchronize previously audited Skill and configuration guidance. Actual Codex-client Roslyn acceptance still awaits a refreshed connection. + ## 0.13.1 - Mask comments/literals before local declarations, add TSX/JSX scanning and scoped context, preserve original signatures/lines and invalidate legacy declaration caches. Uncertain lexical boundaries are incomplete and uncached; text references remain heuristic. diff --git a/README.md b/README.md index 028260c..e481f08 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ WinCode is a local MCP server built for Windows and .NET engineering. It bridges - **Inspect the running app:** Enumerate visible windows, query specific controls or subtrees, and capture numbered visual overlays without activating or stealing focus from the target. - **Review with evidence:** Trace on-screen widgets back to literal XAML declaration tags, line numbers, and file hashes, with transparent reporting for ambiguity, truncation, or degraded upstreams. -Current source version: **0.13.1**. All UI tools are strictly read-only and non-destructive. See [CHANGELOG](CHANGELOG.md) for full version history. +Current source version: **0.13.2**. All UI tools are strictly read-only and non-destructive. See [CHANGELOG](CHANGELOG.md) for full version history. ### Quick start @@ -234,7 +234,7 @@ WinCode 是面向 Windows 与 .NET 工程研发的本地 MCP 服务。它将项 - **观察实际界面:**发现系统可见窗口,按条件定向查询目标控件或子树,并在不激活、不抢占前台焦点的前提下获取数字标注截图。 - **源码双向印证:**将运行时抓取的控件关联回 XAML 源码声明的起始行号、代码片段与文件哈希,清晰报告歧义、截断与降级状态。 -当前源码版本为 **0.13.1**。所有 UI 取证工具均为纯只读与非侵入设计。版本历史见 [CHANGELOG](CHANGELOG.md)。 +当前源码版本为 **0.13.2**。所有 UI 取证工具均为纯只读与非侵入设计。版本历史见 [CHANGELOG](CHANGELOG.md)。 ### 快速上手 diff --git "a/WinCode-Skill\345\210\266\344\275\234\344\270\216MCP\351\205\215\347\275\256\346\214\207\345\215\227.md" "b/WinCode-Skill\345\210\266\344\275\234\344\270\216MCP\351\205\215\347\275\256\346\214\207\345\215\227.md" index 304d97f..47c902a 100644 --- "a/WinCode-Skill\345\210\266\344\275\234\344\270\216MCP\351\205\215\347\275\256\346\214\207\345\215\227.md" +++ "b/WinCode-Skill\345\210\266\344\275\234\344\270\216MCP\351\205\215\347\275\256\346\214\207\345\215\227.md" @@ -1,11 +1,11 @@ # WinCode Skill 安装、维护与 MCP 配置指南 -适用于 **0.13.1**,核对日期 2026-09-08(北京时间)。以下使用本机 `I:/WinCode` 路径举例;其他机器必须替换路径。客户端界面名称随版本变化,以实际界面为准。 +适用于 **0.13.1**,核对日期 2026-09-09(北京时间)。以下使用本机 `I:/WinCode` 路径举例;其他机器必须替换路径。客户端界面名称随版本变化,以实际界面为准。 ## 1. 三个独立对象 - **Skill 手册**指导 Agent 选择工具和使用规范字段,不启动服务器。 -- **磁盘交付物**包含 Gateway、原生 UI Host、构建身份和交付清单。 +- **磁盘交付物**包含 Gateway、原生 UIA/Code Host 的完整发布目录、构建身份和交付清单。 - **MCP 连接实例**是客户端已经启动的进程;更新源码、构建或复制 Skill 都不会自动更新这个进程。 架构与数据流见 [架构说明](WinCode-架构与数据流说明.md)。待办见 [当前计划](WinCode-下一轮工程化迭代计划书.md),不要按历史计划重复安装和升级。 @@ -22,7 +22,7 @@ npm run check npm run delivery:verify ``` -`check` 进行类型检查、Gateway 构建、锁定 NuGet restore、Release UIA/Code Host 与控制台夹具构建、核心回归、新 stdio 验证与交付清单生成。交互桌面验收另执行 `npm run check:desktop`,需要可用 Windows 桌面。真实 Roslyn/TavernDesk 验收按对应入口执行,详见 [CONTRIBUTING](CONTRIBUTING.md)。 +`check` 进行类型检查、Gateway 构建、锁定 NuGet restore、Release UIA/Code Host 与控制台夹具构建、核心回归、新 stdio 验证与交付清单生成。交互桌面验收另执行 `npm run check:desktop`,需要可用 Windows 桌面。真实 Roslyn/TavernDesk 验收按对应入口执行,详见 [CONTRIBUTING](CONTRIBUTING.md)。Roslyn 维护脚本会锁定还原并构建/发布 Code Host,也会还原生成夹具,不还原用户目标应用;NuGet 缓存缺包时可能访问包源,不能将“不自动安装 SDK”理解为完全离线。 `npm run build` 只构建 Gateway,不能单独证明原生 Host、Skill 和整个交付物一致。生产使用发布的 Release Host;`npm run dev` 才显式启用开发回退。报告位于 `test-tmp/check/`,内容哈希不是发布签名。 @@ -77,6 +77,8 @@ npm run skill:check -- C:/Users/40218/.agents/skills/wincode 0.12.1 起 hello 不主动启动探测进程;unknown/null 表示未探测,不代表不可用。已知健康结果也可能陈旧。旧版本 hello 的行为不能套用新版说明。 +实例选用的代码提供方看 `hello.codeProvider`,Roslyn 状态看 `hello.health.roslyn`。`hello.health.text.semanticConfigured=false` 只描述文本适配器,即使启用 Roslyn 仍为 false。`wincode_diagnose_project` 会检查 SDK、探测 Repomix/UIA,但不主动加载 Roslyn 项目;已明确授权的 Roslyn 配置由首次符号搜索触发加载。 + 0.13 系列已经退役外部 Serena,默认 local-text;C# 语义使用随产品交付的 Code Host,通过显式 --roslyn-config 配置入口项目、Configuration、TFM、SDK 与求值许可,字段示例见代码手册。真实 Host/MCP 验收通过也不代表实际客户端已启用 Roslyn。0.12.4 起 Repomix 直接执行已安装 JavaScript bin,不再使用 cmd/npx 包装链,也不会自动下载;非标准安装和降级边界见诊断手册。 ## 6. 常见偏差 @@ -87,6 +89,9 @@ npm run skill:check -- C:/Users/40218/.agents/skills/wincode | 字段被忽略,结果不像预期 | 对照规范字段表和实际工具 schema;容忍未知字段并不赋予其语义 | | Host 缺失或身份不符 | 完整执行锁定构建和 delivery:verify;不混用旧 DLL、新 Gateway 或开发 Host | | Roslyn/Repomix 不可用 | 先核对 provider、显式配置、项目求值许可、已知健康和恢复动作;Roslyn 失败不会暗中换成本地文本,Repomix 降级不代表语义验收成功 | +| Roslyn 返回候选且 references 为空 | 简单名请求未查询引用;按 signature/file/project 消歧,再传返回的 name 与完整 symbolLocation,不能当作零引用 | +| 旧定位报 SNAPSHOT_STALE/INPUTS_CHANGED | 等待写入稳定后显式重新搜索,由适配器执行所需重载,再使用新 location;MCP 没有独立 reload 工具 | +| 限定文件符号取证或 kind 筛选不像预期 | scopeFiles+symbol 支持 .cs/.ts/.tsx/.js/.jsx/.py,属于有限文本匹配;symbol 大小写精确。各后端 kind 类型和大小写规则以代码手册为准,不推定全语言一致 | | UI 查不到或出现多个目标 | 核对 PID/HWND 和大小写准确的查询;只有 complete 且 unique 才能声称唯一定位 | 更新后仍无法核对客户端身份时,保留“客户端未验收”状态与实际证据,不反复尝试未声明参数。 diff --git "a/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" "b/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" index 55cdb10..46b1d4b 100644 --- "a/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" +++ "b/WinCode-\344\270\213\344\270\200\350\275\256\345\267\245\347\250\213\345\214\226\350\277\255\344\273\243\350\256\241\345\210\222\344\271\246.md" @@ -1,6 +1,6 @@ # WinCode 下一轮工程化迭代计划书 -更新日期:2026-09-09(北京时间)。本轮实现版本 **0.13.1**,从 main@a23740c 推进;代码、专项验证及 CI 配置已完成,远端检查和合并以对应 PR 回执为准。实际客户端 Roslyn 验收按用户决定暂缓,不能宣称消费端已经验收。 +更新日期:2026-09-09(北京时间)。本轮实现版本 **0.13.2**,从 main@a23740c 推进;代码、专项验证及 CI 配置已完成,远端检查和合并以对应 PR 回执为准。实际客户端 Roslyn 验收已按用户授权恢复;隔离项目和配置已就绪,等待客户端重连,不能宣称消费端已经验收。 已完成实现从待办移除:LocalText 非代码区屏蔽、有限 TSX/JSX 声明、旧缓存失效、E4 恢复与领域失败专项、未知工具协议错误、影响报告去重及仓内手册同步。过程和失败修复保留在[工作日志](docs/codex_worklog.md),对外变化见 [CHANGELOG](CHANGELOG.md)。 @@ -17,11 +17,11 @@ - 未知工具在正常受理状态返回 JSON-RPC -32602;已知工具失败保留 isError,JSON 文本与 structuredContent 同源。影响分析只有一个 JSON 文本块,保留 formattedReport 和全部证据字段。 - LocalText 仍是有限文本分析:插值/JSX 内表达式省略,复杂语法可能漏检;无法确定词法边界的文件标记 lexical-uncertainty 且不缓存。文本引用不等于语义引用,完整扫描不等于全语言语义完整。 -## 2. 待恢复:实际客户端 Roslyn 验收 +## 2. 进行中:实际客户端 Roslyn 验收 -用户已决定“先完成代码与 CI,客户端验收暂缓”。本轮不修改客户端 MCP 配置,不部署全局 Skill;新 stdio、InMemory、磁盘清单及真实 Host/Gateway 测试均不能代替当前 Codex 连接验收。 +用户在代码与 CI 完成后已授权恢复验收,随后完成 Skill 核对同步。当前已生成隔离 C# 项目、准备 Debug/net10.0 和已安装 SDK 10.0.303,更新现有 wincode 启动参数并备份原服务器配置。Codex CLI 已验证新配置可读取;当前会话仍为 0.12.5,等待刷新对应 MCP 连接。新 stdio、InMemory、磁盘清单及真实 Host/Gateway 测试均不能代替当前 Codex 连接验收。 -恢复时先明确客户端、隔离工作区、入口 csproj、Configuration、单一 TFM、SDK 和 MSBuild 求值范围。推荐使用仓内生成的受控 C# 夹具;若改用 TavernDesk,另行确认真实项目配置和求值授权。 +本次固定入口为生成夹具的 App/App.csproj,求值范围仅为其生成的 App/Lib 项目。包源已清空,fixture restore/build 通过,无新依赖下载。配置及回执位于 test-tmp/client-roslyn-20260909;验收后恢复原 wincode 启动参数。若改用 TavernDesk,另行确认真实项目配置和求值授权。 验收顺序: @@ -31,9 +31,16 @@ 4. 仅修改隔离夹具,用旧 location 验证明确的 stale/输入变化错误;按 recoveryAction 处理并重新搜索,新位置应恢复成功。 5. 记录实际连接身份、位置、错误恢复及结果。手册同步与磁盘文件一致不能替代重连;不得用终止 Codex 或修改真实项目制造验收条件。 -**USER_DECISION_REQUIRED:** 恢复验收时确认上述项目和求值范围。当前暂缓,不影响已经授权的代码、CI、PR 和合并工作。 +**当前待办:** 对应 MCP 连接刷新后,核对新实例及当前交付版本、codeProvider=roslyn、实际隔离工作区和 symbolLocation Schema,再执行上述闭环。配置与求值范围已获授权,不重复申请;未重连前不向旧工具传递新定位字段。 -## 3. 条件性下一轮工作 +## 3. 已授权的生命周期迭代 + +- 0.13.2:已实现 EOF/传输断开、初始化取消、统一关闭预算和故障后的独立资源回收;完整检查及 PR 合并状态以工作日志为准。 +- 下一阶段:验证父进程异常终止、活动 Host/UI 辅助进程回收,明确 Windows Job 的实际保护范围;不把观察到的 PID 退出扩大为所有外部进程保证。 +- 随后:Roslyn 空闲 120 秒实验值、工作区初始化延迟及监听/缓存休眠;唤醒必须核对新鲜度,旧 symbolLocation 明确失效。 +- 保留现有 MCP 接口进程;客户端按需卸载、事件循环完全卡死时的外部兜底须依据实际客户端能力判断。 + +## 4. 条件性下一轮工作 | 方向 | 进入条件与最小实验 | 验收边界 | | --- | --- | --- | @@ -44,9 +51,9 @@ 正式对照保持相同初始信息与任务,字符数不当作实际 token,不将不同模型/提示词的耗时归因于工具。 -## 4. 持续关口 +## 5. 持续关口 - 每个版本先针对性验证和 debug,再执行完整 check、必要的桌面/真实 Roslyn 验收,经过 PR 检查后合并。保留失败证据,不用旧成功覆盖新失败。 - Node 22 CI 执行 E4 专项与真实 Roslyn,Node 22/24 执行核心与交付检查;对应提交的有效 CodeQL 语言均须通过。当前 default-setup 已移除无源码 Python,核对新运行而非为历史红叉添加假源码。 - 当前历史中文文件指纹测试曾偶发失败,后续复现时保留现场,区分测试竞争与真实失效漏洞;不靠删测试或延长 sleep 掩盖问题。 -- 版本、Host、仓内 Skill 与完整磁盘交付必须一致。短时有界负载不等于长期耐久性,异地发布目录不等于干净机器安装,客户端暂缓期间保留“消费者尚未验收”的限制。 +- 版本、Host、仓内 Skill 与完整磁盘交付必须一致。短时有界负载不等于长期耐久性,异地发布目录不等于干净机器安装,客户端闭环完成前保留“消费者尚未验收”的限制。 diff --git "a/WinCode-\350\277\255\344\273\243\350\267\257\347\272\277\345\233\276.md" "b/WinCode-\350\277\255\344\273\243\350\267\257\347\272\277\345\233\276.md" index ffa7e41..9a481f9 100644 --- "a/WinCode-\350\277\255\344\273\243\350\267\257\347\272\277\345\233\276.md" +++ "b/WinCode-\350\277\255\344\273\243\350\267\257\347\272\277\345\233\276.md" @@ -1,12 +1,12 @@ # WinCode 迭代路线图 -更新:2026-09-09(北京时间)。实现版本 **0.13.1**;远端 CI/合并状态查看对应 PR,实际客户端 Roslyn 验收按用户决定暂缓。 +更新:2026-09-09(北京时间)。实现版本 **0.13.1**;远端 CI/合并状态查看对应 PR,实际客户端 Roslyn 验收已恢复,隔离项目与配置准备完成,等待客户端重连。 详细方针和验收条件以[下一轮工程化迭代计划书](WinCode-下一轮工程化迭代计划书.md)为准;完成内容记在 [CHANGELOG](CHANGELOG.md)和[工作日志](docs/codex_worklog.md),不再作为待办重复实施。 ## Next:恢复消费端验收 -先明确隔离 C# 项目的配置与 MSBuild 求值许可,再通过实际客户端完成版本核对、精确符号、引用、影响和旧快照拒绝闭环。当前只完成代码及可自动化的验收,不修改实际客户端或全局 Skill。 +隔离 C# 项目及 MSBuild 求值范围已获准,配置固定 Debug/net10.0,项目构建通过;本地 Skill 已同步,wincode 启动参数已更新并备份。正常重启客户端后完成版本核对、精确符号、引用、影响和旧快照拒绝闭环,结束时恢复原启动参数。当前连接仍为旧版,尚未完成消费端验收。 ## Later:由真实任务决定 diff --git a/docs/codex_worklog.md b/docs/codex_worklog.md index 7c0c6a8..ad01700 100644 --- a/docs/codex_worklog.md +++ b/docs/codex_worklog.md @@ -758,3 +758,49 @@ - 文档:版本统一 0.13.1;README、CHANGELOG、SECURITY、架构/配置指南与仓内 Skill 对齐;计划仅保留暂缓的客户端关口和条件性后续工作。77 个变更文档本地链接、代码围栏核对通过。历史日志追加而非改写,生成证据仍在忽略目录内。 - 作者反证自审覆盖旧缓存假阳性、词法不确定的假零结果、长字面量取消、未知工具后的连接可用、impact 字段/别名保持,以及部分移动后真实文件位置;不等同独立模型或人工审核。剩余边界:有限词法、文本引用、干净机器/长期耐久性及实际消费者均不扩大声明。 - 发布流程:工作分支 codex/local-text-e4-stabilization;本地通过后推送本版本 PR,等待对应 head 的 Node 22/24、真实 Roslyn/E4 及有效 CodeQL 全部通过再合并。远端完成情况以 PR/Actions 回执为准,此条写入时尚未推送,不提前声明 CI 已绿。 + + +## 2026-09-09 21:28 — 本地 WinCode Skill 与远端同步(北京时间) + +- 用户恢复客户端验收后,明确要求先修复远端手册与本地 Skill 脱节。fetch 核对 origin/main 与本地均为 76b343c,四份仓内受管手册与远端一致。 +- 已安装位置为 C:/Users/40218/.agents/skills/wincode,原版本 0.12.5;使用现有 sync-skill 脚本更新 SKILL.md、references/code.md、references/diagnostics.md 到 0.13.1 契约,ui.md 原已一致。修改前完成备份,位置:C:/Users/40218/.agents/skills/.wincode-backup-9404b286-f360-42e9-b6a7-13cd35dbe39d;备份采用 .bak 后缀,不注册为重复 Skill。 +- 更新后四文件 SHA-256 与仓内一致,skill check 无差异;delivery:verify matched=true。详见[本地同步回执](../test-tmp/skill-sync-0131.json)。无新增依赖,未改 MCP 配置、未重启客户端。 +- 实际连接 hello 仍返回 0.12.5,instanceId=17625857-dbe1-48b8-9305-946629711532,wincode_find_references Schema 只有 symbolName/relativePath、没有 symbolLocation。配置仍指向 I:/WinCode/dist/index.js,当前工作区 I:/WinCode,尚未提供 --roslyn-config。因此已完成手册同步,尚未完成实际连接升级或 Roslyn 验收;后续须准备隔离验收配置并正常重连,不能向旧接口传新字段后宣称生效。 + + +## 2026-09-09 21:33 — Skill 功能指引与 0.13.1 实现对照(北京时间) + +- 用户要求确认指引与实际项目是否一致。本轮对照四份受管手册、Gateway Registry/Schema/验证器、LocalText/Context、RoslynAdapter/Host、UI 查询和恢复路径;不使用旧连接行为推定新版源码,不派生子代理。 +- 发现并修正文档差异:scopeFiles+symbol 段落漏写 TSX/JSX;简单名引用示例未提示 Roslyn 只返回候选;health.text.semanticConfigured 被误读为整个实例配置状态的风险;维护脚本实际还原/构建 Host 及生成夹具,不能称为只还原夹具或完全离线。补充不同提供方 kind/大小写与 queryComplete 边界、目录参数默认值、lineRanges.file 长度,以及 diagnose 不主动加载 Roslyn、MCP 搜索代替内部 reload 的具体操作。 +- 验证:源码与 dist 的 15 个公布工具 Schema 哈希一致,四份手册包含所有顶层规范字段;两种别名与恢复规则按源码核对。33 项现有 tool-contracts/local-text 测试全部通过;这不证明未覆盖语法或实际客户端语义。范围及字段清单见[核对回执](../test-tmp/skill-contract-audit.json),[针对性测试输出](../test-tmp/skill-contract-audit-tests.log)。 +- 仅修订仓内 references/code.md 与 references/diagnostics.md,保持公共接口/代码不变。通过现有脚本备份并同步本地 .agents/skills/wincode,四文件相等,Skill 校验、链接/围栏、git diff --check 通过。备份为 .wincode-backup-9fd16a60-2ff6-4bb3-980e-f88fe7f01460。重新生成并验证交付清单 matched=true,contentId=517363f6087c4c8e7976f7d810ae0e70ca290ed2bf6e12a7bf2f60650a1e1694。 +- 反证自审:Roslyn 已配置时 text.semanticConfigured 仍为 false;简单名返回 references=[] 实际可能尚未查询引用。修订指引要求读取 codeProvider/health.roslyn 和 resolution/candidates,并使用真实搜索 location,避免把这两种情况误判为后端不可用或零引用。 +- 再次实际 hello:连接仍是 0.12.5、instanceId=17625857-dbe1-48b8-9305-946629711532;引用 Schema 仅 symbolName/relativePath,没有 symbolLocation。故“手册与本地 0.13.1 实现对齐”不等于“当前连接可使用新版接口”。本轮未改 MCP 配置、未执行客户端 Roslyn 验收;文档修订尚未提交远端。 + + +## 2026-09-09 21:34 — 将核对问题同步到项目 Skill 指南(北京时间) + +- 用户明确要求把发现的问题同步写入项目指南。确认前轮修订已存在于仓内 skills/wincode/references/code.md 与 diagnostics.md,并补齐根目录 WinCode-Skill制作与MCP配置指南.md 的对应说明。 +- 根指南同步健康字段归属、Roslyn 简单名候选/精确定位、过期定位恢复、TSX/JSX 与 kind 差异、维护脚本还原/网络边界;更新核对日期并补齐 Code Host 交付目录说明。参数细节继续引用受管代码手册,不另建重复字段表。 +- 本次仅增加根指南和工作记录,受管四文件未再变化,本地 Skill 与仓内一致;链接/围栏及 git diff --check 核对通过。沿用前轮已通过的 33 项针对性测试,不因说明同步重复运行代码回归。未修改 MCP 配置或推送远端。 + + +## 2026-09-09 21:42 — 恢复实际客户端验收并准备隔离配置(北京时间) + +- 用户要求继续已安排工作,沿用恢复隔离 Roslyn 验收及配置修改的明确授权。保留此前未提交文档修订;不修改生产代码、个人应用数据或真实 TavernDesk 项目。 +- 现场环境与旧记录不同:项目内 .deps/dotnet-10.0.303 已不存在,现有系统 C:/Program Files/dotnet/dotnet.exe 经维护脚本核对 SDK 10.0.303 可用;未安装或修改全局工具链。现有 wincode 参数已由此前的含 workspace 改成仅 dist/index.js,本轮以实际读取的版本备份,未恢复过时配置。 +- 生成 test-tmp/client-roslyn-20260909/workspace 的 App/Lib 项目:Debug、net10.0、C# 13,两个 Api.Save 重载及 Other.Save 同名方法;整型重载基线引用为 App/Use.cs 第 7、10 行,字符串第 8 行、其他类第 9 行。NuGet.Config 清空包源;还原/构建通过。Code Host --identity 返回 0.13.1/Release;delivery:verify matched=true。[准备回执](../test-tmp/client-roslyn-20260909/preparation.json)。 +- 仅修改 C:/Users/40218/.codex/config.toml 的 mcp_servers.wincode.args,加入隔离工作区及 --roslyn-config;其他配置字节保留。原服务器块备份于 C:/Users/40218/.codex/backups/wincode-before-client-roslyn-1b3f90fd-059b-4617-a651-42691eeaadd2.toml,未将完整宿主配置复制进项目。codex mcp get wincode --json 已读出正确新参数。[变更回执](../test-tmp/client-roslyn-20260909/client-config-change.json)。验收完成后恢复此前参数,不保留默认指向测试夹具。 +- 实际连接在配置更新前后均返回 0.12.5、instanceId=17625857-dbe1-48b8-9305-946629711532,引用 Schema 只有 symbolName/relativePath,当前工作区仍 I:/WinCode。没有调用旧版不支持的新参数,没有另起脚本模拟实际消费者通过。当前工具没有重连能力,需用户正常重启 Codex 后继续本任务;不强杀宿主或 Gateway。 +- 当前完成的是夹具构建、配置准备和读取校验;实际搜索/引用/影响/重构、stale 与恢复尚未执行。计划和路线图同步为“进行中,等待客户端重连”,不再沿用暂缓状态。 + + +## 2026-09-09 22:29 — 0.13.2 连接退出与限时清理(北京时间) + +- 用户已确认按顺序实施退出、异常终止验证及空闲回收;沿用逐版本测试、PR、合并授权,单智能体执行,无新依赖。保留并同步此前 Skill/配置指南修订。 +- RED:正式 dist/index.js 在客户端 EOF 后 9 秒仍存活;初始化期间 stop 后仍执行后续阶段;卡住的适配器令其他清理无法执行。 +- GREEN:统一 EOF/close/管道错误与退出入口;先连接传输、业务请求等待初始化;关闭取消所有 MCP 操作及初始化,晚到资源立即回收;共用 8 秒预算并为所有权清理预留时间。 +- 针对性 27 项通过。旧测试预期退出时继续完成工作区重绑,已按批准的取消行为更新为拒绝切换、只执行一次 dispose;保留失败后刷新缓存的既有行为。完整检查、PR/CI 尚待后续记录。 +- 纠正前记录:无需一概重启整个 Codex,刷新对应 MCP 连接即可;当前工具未提供已验证可调用的重连入口,未强杀用户宿主。异常进程终止和空闲回收未在本阶段宣称完成。 + +- 完整检查第一次因已有 40ms TTL 测试在并行负载下先过期而失败(保留 2026-09-09T14-29-49-098Z-core 报告);改用受控 Date.now,保持过期前/后断言,不延长 TTL。重跑核心 322 项:321 通过、1 可选场景跳过;桌面 35/35。正式入口及生命周期最新针对性 27/27,通过交付清单校验。 diff --git a/package-lock.json b/package-lock.json index efa8027..70d87ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wincode-mcp", - "version": "0.13.1", + "version": "0.13.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wincode-mcp", - "version": "0.13.1", + "version": "0.13.2", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", diff --git a/package.json b/package.json index c6f88f5..903fab1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wincode-mcp", - "version": "0.13.1", + "version": "0.13.2", "description": "Windows-first MCP gateway: .NET project graph, evidence-bounded context, honest change-impact, long-running process hygiene", "main": "dist/index.js", "type": "module", @@ -12,7 +12,7 @@ "build": "node scripts/build.mjs", "start": "node dist/index.js", "dev": "tsx src/index.ts --development", - "test": "tsx --test tests/core-cache.test.ts tests/workspace-files.test.ts tests/text-symbols.test.ts tests/context-packing.test.ts tests/composite-tools.test.ts tests/mcp-stdio.test.ts tests/stability-lifecycle.test.ts tests/cache-budgets.test.ts tests/process-failures.test.ts tests/request-concurrency.test.ts tests/evidence-confidence.test.ts tests/watch-invalidation.test.ts tests/resource-cleanup.test.ts tests/stage1-cleanup.test.ts tests/ui-hardening.test.ts tests/ui-source-review.test.ts tests/v071-acceptance.test.ts tests/ui-background.test.ts tests/ui-audit.test.ts tests/ui-query.test.ts tests/context-efficiency.test.ts tests/agent-efficiency-benchmark.test.ts tests/workspace-summary.test.ts tests/runtime-identity.test.ts tests/runtime-contract.test.ts tests/context-coverage.test.ts tests/semantic-identity.test.ts tests/ui-code-candidates.test.ts tests/skill-sync.test.ts tests/local-text.test.ts tests/repomix-disabled.test.ts tests/workspace-watch-close.test.ts tests/workspace-lifecycle.test.ts tests/failure-recovery.test.ts tests/tool-contracts.test.ts tests/architecture-boundaries.test.ts tests/lifecycle-cancellation.test.ts tests/delivery-contract.test.ts tests/roslyn-contracts.test.ts", + "test": "tsx --test tests/core-cache.test.ts tests/workspace-files.test.ts tests/text-symbols.test.ts tests/context-packing.test.ts tests/composite-tools.test.ts tests/mcp-stdio.test.ts tests/stability-lifecycle.test.ts tests/cache-budgets.test.ts tests/process-failures.test.ts tests/request-concurrency.test.ts tests/evidence-confidence.test.ts tests/watch-invalidation.test.ts tests/resource-cleanup.test.ts tests/stage1-cleanup.test.ts tests/ui-hardening.test.ts tests/ui-source-review.test.ts tests/v071-acceptance.test.ts tests/ui-background.test.ts tests/ui-audit.test.ts tests/ui-query.test.ts tests/context-efficiency.test.ts tests/agent-efficiency-benchmark.test.ts tests/workspace-summary.test.ts tests/runtime-identity.test.ts tests/runtime-contract.test.ts tests/context-coverage.test.ts tests/semantic-identity.test.ts tests/ui-code-candidates.test.ts tests/skill-sync.test.ts tests/local-text.test.ts tests/repomix-disabled.test.ts tests/workspace-watch-close.test.ts tests/workspace-lifecycle.test.ts tests/failure-recovery.test.ts tests/tool-contracts.test.ts tests/architecture-boundaries.test.ts tests/lifecycle-cancellation.test.ts tests/delivery-contract.test.ts tests/roslyn-contracts.test.ts tests/gateway-exit.test.ts", "test:verify": "tsx tests/verify.ts", "benchmark:agent": "tsx scripts/benchmark-agent-efficiency.ts", "test:benchmark": "tsx --test tests/agent-efficiency-benchmark.test.ts", diff --git a/skills/wincode/SKILL.md b/skills/wincode/SKILL.md index cf8c344..5150aa2 100644 --- a/skills/wincode/SKILL.md +++ b/skills/wincode/SKILL.md @@ -5,7 +5,7 @@ description: 使用 WinCode MCP 分析 Windows/.NET 工作区,或读取桌面 # WinCode -源码契约:0.13.1(LocalText 词法边界与 E4 错误契约,见对应手册);手册修订:2026-09-09。外部 Serena 入口与旧 source 已退役,不能将此版本号当作当前连接已升级。安装内容可用 `node scripts/sync-skill.mjs <安装目录绝对路径>` 核对;仅维护时执行。以当前连接实际 Schema 为准。 +源码契约:0.13.2(连接退出与限时资源清理,见对应手册);手册修订:2026-09-09。外部 Serena 入口与旧 source 已退役,不能将此版本号当作当前连接已升级。安装内容可用 `node scripts/sync-skill.mjs <安装目录绝对路径>` 核对;仅维护时执行。以当前连接实际 Schema 为准。 默认以本地文本模式启动,source=local-text;明确配置 Roslyn 后,才通过 WinCode.Code.Host 提供 C# 语义证据。搜索返回的 location 可作为引用、影响分析和重构工具的 symbolLocation;不要猜测定位、复用旧快照或使用已退役的 namePath。内部 reload/cancel 不是 MCP 工具字段。配置与验收边界见代码手册。 diff --git a/skills/wincode/references/code.md b/skills/wincode/references/code.md index 395a1ff..17ed96f 100644 --- a/skills/wincode/references/code.md +++ b/skills/wincode/references/code.md @@ -8,7 +8,7 @@ ## 后端与能力边界 -默认 Gateway 使用 WinCode 内置文本能力,`source=local-text`,健康状态明确 semanticConfigured=false。显式启用 Roslyn 后使用直接 Code Host,失败会报错,不会偷偷改换提供方。外部 Serena 连接配置、启动器及旧 `serena-adapter-fallback` 来源已退役;旧调用方须适配。`hello.codeProvider` 标明实例选择,不能根据仓库中存在 Host 推断当前连接已更新。 +默认 Gateway 使用 WinCode 内置文本能力,查询结果 `source=local-text`。`hello.health.text.semanticConfigured=false` 只描述文本适配器,即使启用 Roslyn 仍为 false;整个实例的提供方看 `hello.codeProvider`,Roslyn 状态看 `hello.health.roslyn`。显式启用 Roslyn 后使用直接 Code Host,失败会报错,不会偷偷改换提供方。外部 Serena 连接配置、启动器及旧 `serena-adapter-fallback` 来源已退役;旧调用方须适配。`hello.codeProvider` 标明实例选择,不能根据仓库中存在 Host 推断当前连接已更新。 Roslyn 调用顺序:用 wincode_find_code_symbol 搜索(query 最长 256 字符),根据 signature、file 和 location.project 选择具体声明;再把该项的 name 作为 symbolName、完整 location 对象作为 symbolLocation 传给 wincode_find_references。location 包含 snapshotId(32 位小写十六进制)、project/file(工作区内相对路径)和 position(非负零基 UTF-16)。不手工猜偏移;同名/重载返回候选,不能自动选第一项。简单名称查询在当前不完整范围下只返回候选,单候选也需明确定位;candidatesTruncated=true 时 candidateCount 可能缺省,不能当作全量计数。 @@ -31,7 +31,7 @@ Roslyn 调用顺序:用 wincode_find_code_symbol 搜索(query 最长 256 字 allowProjectEvaluation 表示允许 MSBuild 设计时求值执行项目 targets,须符合用户授权;不会自动 restore 或下载 SDK。project 是相对当前工作区的固定入口;A→B 切换后使用 B 中同一路径,缺失就报错,不猜其他项目。配置和 TFM 当前固定于实例,要改变它们需更新启动配置并重启 Gateway。dotnetPath/hostPath 必须为绝对普通文件,重解析路径不支持;子进程使用指定 dotnet 的安装根,不改系统环境。可选 loadTimeoutMs 为 1–120000(默认 120000),queryTimeoutMs 为 1–60000(默认 30000),不属于 MCP 请求参数。 -维护验收使用 `npm run test:roslyn-host`(独立 Host)和 `npm run test:roslyn-gateway`(真实 stdio MCP)。维护脚本按显式 WINCODE_DOTNET_PATH、项目 .deps、DOTNET_HOST_PATH、PATH 顺序寻找已安装 SDK,并核对 global.json 的精确版本;不下载安装。当前要求 10.0.303。这些脚本只还原生成夹具,保留 test-tmp 报告;Gateway 验收使用完整发布目录的异地副本。此验收不证明当前 Codex 连接已更新或无 SDK 的机器可运行。 +维护验收使用 `npm run test:roslyn-host`(独立 Host)和 `npm run test:roslyn-gateway`(真实 stdio MCP)。维护脚本按显式 WINCODE_DOTNET_PATH、项目 .deps、DOTNET_HOST_PATH、PATH 顺序寻找已安装 SDK,并核对 global.json 的精确版本;不下载安装。当前要求 10.0.303。这些脚本会锁定还原并构建/发布 WinCode Code Host,也会还原生成的测试夹具,保留 test-tmp 报告;不还原用户目标应用。现有 NuGet 缓存缺包时还原可能访问包源,不能将“不下载 SDK”理解成完全离线。Gateway 验收使用完整发布目录的异地副本。此验收不证明当前 Codex 连接已更新或无 SDK 的机器可运行。 `additionalInputs` 是可选启动配置,默认空数组。例如自定义构建读取现存的 `schema.yaml` 和非标准导入 `build-inputs/custom.rules`,可填 `["schema.yaml","build-inputs/custom.rules"]`。最多 32 个工作区相对文件路径,数组 JSON 最长 4096 个 UTF-16 字符;不接受根外/绝对路径、重复项、目录、通配符或链接。缺失项报 INPUT_UNAVAILABLE,不静默删除;创建或恢复文件后再显式搜索。切换工作区后列表按新根解释,各根均须具备所列文件。修改列表需更新启动配置并重启 Gateway,普通 MCP 参数不能添加输入或获取项目执行许可。 @@ -60,14 +60,25 @@ Host 监听变化并在查询前后比较内容指纹,变化时丢弃结果并 | 工具 | 必填字段 | 可选字段及类型 | | --- | --- | --- | | `workspace_open` | `path`: 非空字符串,最长 4096 | `includeTree`: 布尔值;`maxOutputChars`: 整数 2048–32768,默认 8000 | -| `wincode_list_directory` | 无 | `path`: 非空字符串,默认 `.`;`maxDepth`: 整数 1–5;`maxEntries`: 整数 1–500;`maxOutputChars`: 整数 2048–32768;`includeIgnored`: 布尔值 | +| `wincode_list_directory` | 无 | `path`: 非空字符串,最长 4096,默认 `.`;`maxDepth`: 整数 1–5,默认 1;`maxEntries`: 整数 1–500,默认 100;`maxOutputChars`: 整数 2048–32768,默认 8000;`includeIgnored`: 布尔值,默认 false | | `wincode_analyze_workspace` | 无 | `maxDepth`: 数字,默认 2 | -| `wincode_find_code_symbol` | `query`: 非空字符串 | `kind`: 字符串,常用 `class/interface/method/function/type/enum`;此工具未声明文件范围参数,指定文件取证改用下面的 `scopeFiles` | -| `wincode_find_references` | `symbolName`: 非空字符串 | `relativePath`: 定义文件相对路径;`symbolLocation`: Roslyn 搜索返回的 location 对象(snapshotId/project/file/position 均必填,路径各最长 4096);同时提供 relativePath 时必须与 location.file 一致 | +| `wincode_find_code_symbol` | `query`: 非空字符串 | `kind`: 字符串,按下述提供方支持范围使用;Roslyn 的 query 最长 256、kind 最长 128。此工具未声明文件范围参数,指定文件取证改用下面的 `scopeFiles` | +| `wincode_find_references` | `symbolName`: 非空字符串 | `relativePath`: 定义文件相对路径(Roslyn 用于限定候选,local-text 不据此缩小引用扫描);`symbolLocation`: Roslyn 搜索返回的 location 对象(snapshotId/project/file/position 均必填,路径各最长 4096);同时提供 relativePath 时必须与 location.file 一致 | | `analyze_change_impact` | `target`: 非空字符串 | `symbolLocation`: 搜索返回的完整定位;提供时 target 必须是该符号的简单名称 | | `wincode_plan_refactoring` | `target`、`goal`: 非空字符串 | `symbolLocation`: 同影响分析 | | `wincode_safe_move_to_trash` | `filePath`: 工作区内相对路径字符串 | `reason`: 字符串;该工具实际移动文件,须符合用户授权 | +`kind` 不是全语言统一的能力承诺:local-text 的 C# 模式匹配 class/struct/interface/enum/method,TS/TSX/JS/JSX 模式匹配 class/interface/enum/type/function/method,Python 模式匹配 class/function;均为有限单行声明模式。Roslyn 返回 class/interface/struct/enum/type/method/property,构造函数归入 method,`type` 筛选也匹配命名类型。Roslyn kind 大小写精确,local-text kind 忽略大小写;建议统一使用上述小写值。两种提供方的 query 都是忽略大小写的名称子串搜索;prepare_context 的 symbol 则是大小写精确匹配。 + +Roslyn 精确调用示意(`selected` 必须是本次搜索结果中经消歧选定的项,不能照抄虚构定位): + +```javascript +wincode_find_references({symbolName: selected.name, symbolLocation: selected.location}) +wincode_analyze_change_impact({target: selected.name, symbolLocation: selected.location}) +``` + +local-text 的 queryComplete=true 仅表示该次有界文本扫描完成,仍是 degraded,不证明语义完整;Roslyn 当前 queryComplete 始终为 false,应结合 resolution、semanticContext 和实际返回证据判断。 + `wincode_analyze_change_impact` 是 `analyze_change_impact` 的公布别名;`wincode_workspace_open` 是 `workspace_open` 的历史兼容别名。别名共享参数和执行规则,优先使用本连接 tools/list 公布的名称。其余字段名不接受自动拼写纠正。 `wincode_prepare_context` 的完整规范字段如下。路径均为工作区内的字面路径,不是 glob。 @@ -78,7 +89,7 @@ Host 监听变化并在查询前后比较内容指纹,变化时丢弃结果并 | `candidateFiles` | 可选字符串数组,最多 20,每项最长 1024 | 优先候选,**不排他**;与 `scopeFiles` 同用时须在其内 | | `scopeFiles` | 可选字符串数组,1–20,每项最长 1024 | 排他范围;不能与 `focusAreas` 同用 | | `symbol` | 可选字符串,最长 128,不含空白 | 大小写精确声明名;必须有 `scopeFiles`,不能与 `lineRanges` 同用 | -| `lineRanges` | 可选对象数组,1–8 | 每项必填 `file`(字符串)、`startLine/endLine`(正整数);1 起始闭区间、起点≤终点、每段≤500行,每文件仅一段;若有 scope,必须在 scope 内;不能与 `symbol` 或 `includeFullText:true` 同用 | +| `lineRanges` | 可选对象数组,1–8 | 每项必填 `file`(非空字符串,最长 1024)、`startLine/endLine`(正整数);1 起始闭区间、起点≤终点、每段≤500行,每文件仅一段;若有 scope,必须在 scope 内;不能与 `symbol` 或 `includeFullText:true` 同用 | | `focusAreas` | 可选字符串数组,最多 5,每项最长 1024 | 文件或目录;不支持通配符 | | `compress` | 可选布尔值 | 仅在全文且实际使用 CLI 时转发压缩选项;内置降级不做 AST 压缩 | | `outputFormat` | 可选字符串 `markdown/xml` | 默认 `markdown`,用于打包正文 | @@ -100,7 +111,7 @@ lineRanges 查看最终 coverage.allRequestedCovered、completeLines 和 details | 按需浏览目录 | wincode_list_directory({path: "src", maxDepth: 1, maxEntries: 100}) | | 项目依赖概览 | wincode_analyze_workspace({maxDepth: 2}) | | 找符号 | wincode_find_code_symbol({query: "Save"}) | -| 查引用 | wincode_find_references({symbolName: "Save", relativePath: "src/Service.cs"}) | +| 文本引用线索 / Roslyn 候选消歧 | wincode_find_references({symbolName: "Save", relativePath: "src/Service.cs"});Roslyn 未传 symbolLocation 时只返回候选,不查询引用 | | 变更影响 | wincode_analyze_change_impact({target: "Service"}) | | 重构步骤 | wincode_plan_refactoring({target: "Service", goal: "拆分保存逻辑"}) | @@ -130,7 +141,7 @@ lineRanges 为闭区间、1 起始行号,最多 8 个文件,每文件一个 维护者可运行 npm run benchmark:agent -- 1 做单轮检查,或 -- 3 做三轮对照;报告在 test-tmp/agent-efficiency。它比较十类固定脚本场景(含既有 C# 夹具)的调用、返回字符、重复显示行和证据断言,使用真实 MCP handler 与本地回退,关闭外部后端。数据不代表真实用户任务频率、模型完成率或缓存收益,不据此宣称通用提速。Schema v2 校验当前文件、行号、正文和状态,异常保留为失败记录;复用只依赖受控夹具的可信无变化事件,修改后必须重取,不能作为生产环境的新鲜度判断。 -仅知道文件时用 scopeFiles:["src/Service.cs"] 排他限定最多 20 个文件;它跳过全仓库符号搜索,不能与 focusAreas 同用,candidateFiles/lineRanges 必须在其内。需要声明附近片段可加 symbol:"Save":大小写精确匹配,必须提供 scopeFiles,目前复用 C#/TS/JS/Python 本地声明模式,并非语义解析,queryComplete=false。重名、未找到或不支持语言会返回 fileIssues,不用文件开头冒充命中;可用已知行号进一步消歧。 +仅知道文件时用 scopeFiles:["src/Service.cs"] 排他限定最多 20 个文件;它跳过全仓库符号搜索,不能与 focusAreas 同用,candidateFiles/lineRanges 必须在其内。需要声明附近片段可加 symbol:"Save":大小写精确匹配,必须提供 scopeFiles,目前复用 .cs/.ts/.tsx/.js/.jsx/.py 本地声明模式,并非语义解析,queryComplete=false。重名、未找到或不支持语言会返回 fileIssues,不用文件开头冒充命中;可用已知行号进一步消歧。 metrics.selectedFiles 是选择数,packedFiles 是打包器实际处理数(片段模式为片段数),returnedFiles 是返回正文覆盖数;打包器缺少正文位置时为 null。relatedFiles.bodyStatus 表示 complete/partial/omitted/unknown;片段模式的 complete 仅表示该片段完整,不表示整个文件完整。小预算先裁辅助列表,metadataTruncated 提示列表可能不全。 diff --git a/skills/wincode/references/diagnostics.md b/skills/wincode/references/diagnostics.md index 8b86413..97c6fc2 100644 --- a/skills/wincode/references/diagnostics.md +++ b/skills/wincode/references/diagnostics.md @@ -4,7 +4,9 @@ 从 0.12.4 起 Repomix 健康探测和打包都由当前 Node 可执行文件直接启动已安装的 JavaScript CLI;不经过 cmd、npx 或 PATH 包装脚本,也不下载包。默认按目标工作区和 WinCode 安装目录的 Node 模块路径读取 repomix/package.json 的 bin 入口;不搜索 npx 缓存或 npm 自定义全局前缀。非标准安装需在宿主 WinCodeConfig.adapters.repomix.customCliPath 提供绝对 .js/.cjs/.mjs 路径;该字段不是 MCP 工具参数,不能传给 hello/prepare_context。显式路径无效时返回 builtin fallback,不执行另一份安装;useCli=false 仍完全禁止探测和启动。执行已安装脚本不提供沙盒或脚本可信性保证。 -`hello` 从 0.12.1 起只读取版本、能力和已知状态,不启动上游、CLI 或 UI Host 探测进程。`health.healthObservation` 区分 `known/unknown` 并给出 `observedAt`;`unknown`、`available:null` 或 `commandFound:null` 表示尚未探测,不能解释为不可用。配置禁用属于已知策略,但观察时间可为 null。已知健康结果可能陈旧,需要当前检查时调用现有 `wincode_diagnose_project({})`,不向 hello 添加未声明的 force/probe 字段。 +`hello` 从 0.12.1 起只读取版本、能力和已知状态,不启动上游、CLI 或 UI Host 探测进程。`health.healthObservation` 区分 `known/unknown` 并给出 `observedAt`;`unknown` 或 `available:null` 表示尚未探测,不能解释为不可用。配置禁用属于已知策略,但观察时间可为 null。已知健康结果可能陈旧,需要当前检查时调用现有 `wincode_diagnose_project({})`,不向 hello 添加未声明的 force/probe 字段。 + +`wincode_diagnose_project` 会检查 SDK 并主动探测 Repomix/UIA;对 Roslyn 只读取已有加载状态,不会启动 Code Host 或执行项目加载。已授权配置 Roslyn 后,首次明确的符号搜索才触发加载。`health.healthObservation` 当前包含 text/repomix/flaui;Roslyn 的观察时间与快照状态在 `health.roslyn`,不要按旧 Serena 字段判断。 代码查询、引用、上下文、影响分析和重构建议接收 MCP 客户端取消信号;停止后续扫描/打包,等待当前读操作或自有上游进程清理后释放请求占用。Roslyn 取消会传播到自有 Host;若合作取消未及时完成,则按既有超时策略清理自有进程树,不宣称其他请求已成功。磁盘单次 OS I/O 不能保证瞬时中断。工作区切换在等待和提交前可取消;已开始提交切换时完成一致性收尾,不声称已回滚。 @@ -44,7 +46,7 @@ WORKSPACE_RECOVERY_REQUIRED 表示切换中途失败后工作区一致性尚未 Roslyn 运行中已观察到的加载、查询或清理错误也纳入 health.lastAdapterError,provider=roslyn;health.roslyn.health.lastError 保留对应观察。lastError 是历史最后一次失败,不表示每次 hello 都执行了健康探测,也不能据此自行重放业务请求。工作区完整重置后观察清空。 -Code Host 内部协议 v2 的失败包含 success=false、errorCode 和 error,且不附带旧引用。SNAPSHOT_STALE/INPUTS_CHANGED 要求等写入稳定后显式 reload,再用新身份定位;PROJECT_LOAD_FAILED 表示结构化 MSBuild 加载失败,先修复项目输入,再 reload,不能继续使用最后一次成功快照。源码的 compilationErrors 可随有用的部分引用返回,不能据此宣称完整。 +Code Host 内部协议 v2 的失败包含 success=false、errorCode 和 error,且不附带旧引用。SNAPSHOT_STALE/INPUTS_CHANGED 在内部协议层要求等写入稳定后显式 reload,再用新身份定位;MCP 客户端应重新调用 wincode_find_code_symbol,由适配器执行所需重载,不存在 wincode_reload 工具;PROJECT_LOAD_FAILED 表示结构化 MSBuild 加载失败,先修复项目输入,再 reload,不能继续使用最后一次成功快照。源码的 compilationErrors 可随有用的部分引用返回,不能据此宣称完整。 Roslyn 的已知领域错误通过 MCP 的 isError=true 和 JSON 文本 success=false/errorCode/errorMessage 返回;失败的 JSON 文本与 structuredContent 一致,仍保留领域差异。HOST_RESTART_REQUIRED(SDK/监听状态)应对当前路径执行 workspace_open,再显式搜索;同根打开也关闭旧 Host 后重新选择 SDK。清理失败则按 WORKSPACE_RECOVERY_REQUIRED 的 restart_gateway 处理,不能通过再次打开恢复。HOST_TIMEOUT/HOST_CRASHED 后旧定位不可用,下一次显式搜索才启动新 Host;不会重放失败引用。 @@ -54,3 +56,7 @@ INPUT_UNAVAILABLE/HOST_UNAVAILABLE 先检查明确的配置文件、SDK/Host/项 `npm run test:error-contracts` 使用生成夹具验证错误、部分完成与恢复;Node 22 CI 执行该专项并保存有界报告。UI 图片场景使用注入响应,只验证序列化,不冒充真实屏幕验收。 + +## 连接关闭(0.13.2) + +正式入口在 stdin EOF/close、传输关闭或管道错误时停止接收请求,取消初始化和活动操作,并按统一 8 秒预算清理自有资源。关闭失败保留非零退出结果;缓存写入不能无限延迟退出。不能把此行为等同于 Codex 当前连接已更新,也不能承诺强杀 Gateway 时所有后代均受同一个 Windows Job 保护。升级后刷新对应 MCP 连接,不必一概重启整个 Codex。 diff --git a/src/Core/Config.ts b/src/Core/Config.ts index adc9974..a35bcb4 100644 --- a/src/Core/Config.ts +++ b/src/Core/Config.ts @@ -1,6 +1,6 @@ import path from 'node:path'; -export const WINCODE_VERSION = '0.13.1'; +export const WINCODE_VERSION = '0.13.2'; /** * Bounded waits for every external process/RPC. None of these may be Infinity. diff --git a/src/Core/ResourceManager.ts b/src/Core/ResourceManager.ts index 3fa1ec2..3759b91 100644 --- a/src/Core/ResourceManager.ts +++ b/src/Core/ResourceManager.ts @@ -257,6 +257,10 @@ export class ResourceManager { private omittedCloseResults = 0; private readonly lateCleanups = new Set>(); private readonly lateFailures: Error[] = []; + private closeDeadline = Infinity; + + /** Newly acquired resources must be disposed immediately, including late initialization results. */ + seal(): void { this.disposed = true; } getCloseReport() { return { results: this.closeResults.map(result => ({ ...result })), omitted: this.omittedCloseResults }; @@ -270,7 +274,11 @@ export class ResourceManager { } private async drainLateCleanups(): Promise { - while (this.lateCleanups.size) await Promise.all([...this.lateCleanups]); + while (this.lateCleanups.size) { + const pending = Promise.all([...this.lateCleanups]); + if (Number.isFinite(this.closeDeadline)) await withTimeout(pending, Math.max(1, this.closeDeadline - Date.now()), 'late-resource-cleanup'); + else await pending; + } if (this.lateFailures.length) throw new AggregateError(this.lateFailures, 'Late resource cleanup failed.'); } @@ -327,9 +335,10 @@ export class ResourceManager { /** * Safe to call twice. Concurrent callers share the same in-flight dispose. */ - async dispose(): Promise { + async dispose(deadline = Infinity): Promise { if (this.disposePromise) { await this.disposePromise; await this.drainLateCleanups(); return; } this.disposed = true; + this.closeDeadline = deadline; this.disposePromise = this.disposeOnce(); return this.disposePromise; } @@ -337,9 +346,12 @@ export class ResourceManager { private async disposeOnce(): Promise { const items = Array.from(this.resources.values()).reverse(); const failures: Error[] = []; - for (const item of items) { + for (const [index, item] of items.entries()) { try { - await item.dispose(); + const pending = Promise.resolve().then(() => item.dispose()); + if (Number.isFinite(this.closeDeadline)) + await withTimeout(pending, Math.max(1, (this.closeDeadline - Date.now()) / (items.length - index)), `close-${item.owner}`); + else await pending; this.resources.delete(item.id); this.recordClose(item); } catch (error) { diff --git a/src/Core/ToolRouter.ts b/src/Core/ToolRouter.ts index 0ab577c..020e7a9 100644 --- a/src/Core/ToolRouter.ts +++ b/src/Core/ToolRouter.ts @@ -15,7 +15,7 @@ import { ImpactAnalyzer } from '../CompositeTools/ImpactAnalyzer.js'; import { RefactorAssistant } from '../CompositeTools/RefactorAssistant.js'; import { ProjectDiagnostics } from '../CompositeTools/ProjectDiagnostics.js'; import { ExtensionManager } from '../Extensions/ExtensionManager.js'; -import { Mutex, ResourceManager, AbortError, TimeoutError, GatewayRestartRequiredError } from './ResourceManager.js'; +import { Mutex, ResourceManager, AbortError, TimeoutError, GatewayRestartRequiredError, withTimeout } from './ResourceManager.js'; import { SessionManager, WorkspaceSession } from './SessionManager.js'; import { WorkspaceWatch } from './WorkspaceWatch.js'; import { AdapterLastError } from './AdapterStatus.js'; @@ -93,6 +93,8 @@ export class ToolRouter { private shuttingDown = false; private inFlight = 0; private disposePromise: Promise | null = null; + private initialization: Promise | null = null; + private readonly shutdownController = new AbortController(); private readonly workspaceLock = new Mutex(); private switchingPromise: Promise | null = null; private resolveSwitching: (() => void) | null = null; @@ -152,6 +154,12 @@ export class ToolRouter { return this.shuttingDown; } + get shutdownSignal(): AbortSignal { return this.shutdownController.signal; } + + private assertActive(): void { + if (this.shuttingDown) throw new AbortError('WinCode is shutting down; operation cancelled.'); + } + /** 提供方在构造时显式选择;Roslyn 失败不触发 Serena RPC。 */ private get code(): LocalTextAdapter | RoslynAdapter { return this.roslyn ?? this.text; } @@ -251,7 +259,11 @@ export class ToolRouter { } async initialize(): Promise { - try { await this.initializeOnce(); } + try { + this.assertActive(); + this.initialization ??= this.initializeOnce(); + await this.initialization; + } catch (error) { try { await this.dispose(); } catch (cleanup) { throw new AggregateError([error, cleanup], 'Initialization and cleanup failed.'); } @@ -262,12 +274,15 @@ export class ToolRouter { private async initializeOnce(): Promise { this.cache.setNamespace(this.config.workspaceRoot); this.session.open(this.config.workspaceRoot, this.cache.currentNamespace); - await this.cache.initialize(); - await this.repomix.initialize(); - if (!this.roslyn) await this.text.initialize(); - await this.flaui.initialize(); - await this.extensions.initializeAll(); + for (const initialize of [() => this.cache.initialize(), () => this.repomix.initialize(), + () => this.roslyn ? Promise.resolve() : this.text.initialize(), () => this.flaui.initialize(), + () => this.extensions.initializeAll()]) { + this.assertActive(); + await initialize(); + this.assertActive(); + } const fp = await this.cache.computeWorkspaceFingerprint(this.config.workspaceRoot); + this.assertActive(); this.session.setFingerprint(fp); await this.bindWatch(this.config.workspaceRoot); @@ -283,6 +298,7 @@ export class ToolRouter { private async bindWatch(workspaceRoot: string): Promise { try { await this.watch.stop(); } catch (error) { throw new GatewayRestartRequiredError([error], 'Workspace watcher cleanup failed; restart the Gateway after checking cleanup.'); } + this.assertActive(); this.watch.start(workspaceRoot, () => { this.cache.noteFilesystemChange(workspaceRoot); }); @@ -304,6 +320,7 @@ export class ToolRouter { * provider cleanup/reinitialization and cache namespace changes. */ async openWorkspace(targetPath: string, options: WorkspaceOpenOptions = {}, signal?: AbortSignal) { + signal = signal ? AbortSignal.any([signal, this.shutdownSignal]) : this.shutdownSignal; return this.workspaceLock.runExclusive(async () => { if (this.shuttingDown) { throw new Error('WinCode is shutting down; workspace_open rejected.'); @@ -511,6 +528,8 @@ export class ToolRouter { async dispose(): Promise { if (this.disposePromise) return this.disposePromise; this.shuttingDown = true; + this.shutdownController.abort(); + this.resources.seal(); for (const controller of this.codeOperations) controller.abort(); this.disposePromise = this.disposeOnce(); // Retain the settled result: repeated callers must not see success after failed cleanup. @@ -519,32 +538,44 @@ export class ToolRouter { private async disposeOnce(): Promise { this.shuttingDown = true; - // Let any active switch finish before disposing the resources it binds. - return this.workspaceLock.runExclusive(async () => { - const drainMs = Math.min(3_000, this.config.timeouts?.shutdownMs ?? 8_000); - const drained = await this.waitForIdle(drainMs); + const budget = this.config.timeouts?.shutdownMs ?? 8_000; + const deadline = Date.now() + budget; + const softDeadline = deadline - Math.min(2_000, budget / 4); + const failures: unknown[] = []; + const attempt = async (name: string, work: () => Promise | void, limit: number) => { + try { await withTimeout(Promise.resolve().then(work), Math.max(1, limit), name); } + catch (error) { failures.push(error); } + }; + // A stuck initializer/switch cannot monopolize the process-wide exit deadline. + await attempt('shutdown-drain', async () => { + await this.initialization?.catch(() => {}); + await this.switchingPromise?.catch(() => {}); + const drained = await this.waitForIdle(Math.max(1, Math.min(3_000, softDeadline - Date.now()))); + if (!drained) throw new Error('Requests did not settle before shutdown.'); + }, Math.min(3_000, (softDeadline - Date.now()) / 3)); + const drained = failures.length === 0; if (this.pruneTimer) { clearInterval(this.pruneTimer); this.pruneTimer = null; } - const failures: unknown[] = []; - if (!drained) failures.push(new Error(`Requests did not settle within shutdown drain (${drainMs}ms).`)); // Every owner gets a cleanup attempt even if a previous adapter failed. // Keep ordering: adapters stop producing work before queued cache writes drain. - for (const cleanup of [ + const cleanups = [ () => this.watch.stop(), () => this.repomix.dispose(), () => this.text.dispose(), () => this.roslyn?.dispose(), () => this.flaui.dispose(), () => this.extensions.disposeAll(), - () => this.cache.flush(), + // Never race a cache flush against an initializer or request that failed to drain. + () => drained ? this.cache.flush() : Promise.resolve(), () => this.session.close(), - () => this.resources.dispose(), - ]) { - try { await cleanup(); } catch (error) { failures.push(error); } + ]; + for (const [index, cleanup] of cleanups.entries()) { + await attempt(`shutdown-owner-${index}`, cleanup, (softDeadline - Date.now()) / (cleanups.length - index)); } + // Independent ownership cleanup still runs when an adapter has hung or thrown. + await attempt('shutdown-owned-resources', () => this.resources.dispose(deadline), deadline - Date.now()); if (failures.length) throw new AggregateError(failures, 'One or more gateway resources failed to close.'); - }); } } diff --git a/src/Gateway/McpServer.ts b/src/Gateway/McpServer.ts index edc531a..38a5114 100644 --- a/src/Gateway/McpServer.ts +++ b/src/Gateway/McpServer.ts @@ -10,18 +10,22 @@ import { toolErrorResult, codeRecoveryAction, type ToolExecutionContext } from ' export class WinCodeMcpServer { private server: Server; private stopPromise: Promise | null = null; + private startPromise: Promise | null = null; + onDisconnect?: () => void; private readonly registry = new ToolRegistry(); constructor(private readonly router: ToolRouter) { this.server = new Server({ name: 'wincode-agent-gateway', version: WINCODE_VERSION }, { capabilities: { tools: {} } }); + this.server.onclose = () => this.onDisconnect?.(); this.registerHandlers(); } private registerHandlers(): void { this.server.setRequestHandler('tools/list', async () => ({ tools: this.registry.list() })); this.server.setRequestHandler('tools/call', async (request, ctx) => { + await this.startPromise?.catch(() => {}); const { name, arguments: input = {} } = request.params; - const signal = ctx.mcpReq.signal; + const signal = ctx.mcpReq.signal ? AbortSignal.any([ctx.mcpReq.signal, this.router.shutdownSignal]) : this.router.shutdownSignal; if (this.router.isShuttingDown || signal?.aborted) { return toolErrorResult(this.router.isShuttingDown ? 'SHUTDOWN' : 'CANCELLED', this.router.isShuttingDown ? 'WinCode is shutting down; tool call rejected.' : 'Tool call was cancelled.', @@ -63,10 +67,17 @@ export class WinCodeMcpServer { }); } - async start(): Promise { - await this.router.initialize(); + start(): Promise { + this.startPromise ??= this.startOnce(); + return this.startPromise; + } + + private async startOnce(): Promise { + // Connect first so EOF can cancel even a slow initialization; tool calls await startPromise. const transport = new StdioServerTransport(); await this.server.connect(transport); + await this.router.initialize(); + if (this.router.isShuttingDown) throw new AbortError('Gateway startup cancelled.'); console.error(`[WinCode Gateway] MCP Server ${WINCODE_VERSION} running on stdio transport.`); } diff --git a/src/index.ts b/src/index.ts index 3de2741..078125a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,44 +39,58 @@ async function main() { const server = new WinCodeMcpServer(router); let shuttingDown = false; - const shutdown = async (signal: string) => { - if (shuttingDown) return; + let shutdownPromise: Promise | undefined; + const shutdown = (signal: string, exitCode = 0): Promise => { + if (shutdownPromise) return shutdownPromise; shuttingDown = true; - console.error(`[WinCode Gateway] ${signal}: shutting down...`); - const force = setTimeout(() => { - console.error('[WinCode Gateway] Shutdown timed out; exiting.'); - process.exit(1); - }, config.timeouts.shutdownMs); - force.unref(); - try { - await server.stop(); - process.exit(0); - } catch (err) { - console.error('[WinCode Gateway] Error during shutdown:', err); - process.exit(1); - } + shutdownPromise = (async () => { + console.error(`[WinCode Gateway] ${signal}: shutting down...`); + const force = setTimeout(() => { + console.error('[WinCode Gateway] Shutdown timed out; exiting.'); + process.exit(1); + }, config.timeouts.shutdownMs); + force.unref(); + try { + await server.stop(); + process.exit(exitCode); + } catch (err) { + console.error('[WinCode Gateway] Error during shutdown:', err); + process.exit(1); + } + })(); + return shutdownPromise; }; + server.onDisconnect = () => { void shutdown('transport closed'); }; + process.on('SIGINT', () => { void shutdown('SIGINT'); }); process.on('SIGTERM', () => { void shutdown('SIGTERM'); }); + process.stdin.once('end', () => { void shutdown('stdin EOF'); }); + process.stdin.once('close', () => { void shutdown('stdin closed'); }); + process.stdin.once('error', () => { void shutdown('stdin error'); }); + process.stdout.once('error', () => { void shutdown('stdout error'); }); + // The client may close its diagnostic pipe too; logging must not recursively crash shutdown. + process.stderr.on('error', () => {}); process.on('uncaughtException', (err) => { - console.error('[WinCode Gateway] Uncaught exception (gateway stays up unless shutdown fails):', err); + console.error('[WinCode Gateway] Uncaught exception:', err); + void shutdown('uncaughtException', 1); }); process.on('unhandledRejection', (err) => { - console.error('[WinCode Gateway] Unhandled rejection (gateway stays up):', err); + console.error('[WinCode Gateway] Unhandled rejection:', err); + void shutdown('unhandledRejection', 1); }); try { await server.start(); console.error(`[WinCode Gateway] v${WINCODE_VERSION} ready.`); } catch (err) { + if (shuttingDown) { await shutdownPromise; return; } console.error('[WinCode Gateway] Fatal error starting server:', err); - await server.stop().catch(() => {}); - process.exit(1); + await shutdown('startup failed', 1); } } diff --git a/tests/cache-budgets.test.ts b/tests/cache-budgets.test.ts index a698b3b..e78cdab 100644 --- a/tests/cache-budgets.test.ts +++ b/tests/cache-budgets.test.ts @@ -30,12 +30,14 @@ describe('cache-budgets', () => { await fs.rm(testCacheDir, { recursive: true, force: true }).catch(() => { }); }); describe('Cache', () => { - it('TTL still expires entries', async () => { + it('TTL still expires entries', async t => { const cache = new CacheManager(path.join(testCacheDir, 'ttl')); await cache.initialize(); + let now = Date.now(); + t.mock.method(Date, 'now', () => now); await cache.set('t', { n: 1 }, { ttlMs: 40 }); assert.ok(await cache.get('t')); - await new Promise((r) => setTimeout(r, 60)); + now += 60; assert.strictEqual(await cache.get('t'), null); }); diff --git a/tests/gateway-exit.test.ts b/tests/gateway-exit.test.ts new file mode 100644 index 0000000..8cec3ec --- /dev/null +++ b/tests/gateway-exit.test.ts @@ -0,0 +1,66 @@ +import { it } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'; +import { killProcessTree, withTimeout } from '../src/Core/ResourceManager.js'; + +async function entryFixture(run: (child: ChildProcessWithoutNullStreams, exit: Promise, logs: () => string) => Promise) { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wincode-entry-exit-')); + const child = spawn(process.execPath, [path.resolve('dist/index.js'), '--workspace', root], + { cwd: root, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }); + let logs = ''; + child.stderr.on('data', data => { logs = (logs + String(data)).slice(-16000); }); + child.stdin.on('error', () => {}); + const exit = new Promise((resolve, reject) => { child.once('close', resolve); child.once('error', reject); }); + try { await run(child, exit, () => logs); } + finally { + if (child.exitCode === null && child.signalCode === null) await killProcessTree(child); + await withTimeout(exit, 5000, 'fixture-reap'); + await fs.rm(root, { recursive: true, force: true }); + } +} + +async function handshake(child: ChildProcessWithoutNullStreams) { + let buffer = ''; + const ready = new Promise((resolve, reject) => { + const read = (data: Buffer) => { + buffer += data.toString(); + const lines = buffer.split('\n'); buffer = lines.pop()!; + for (const line of lines) { + const message = JSON.parse(line); + if (message.id === 1) { + child.stdout.off('data', read); + if (message.error) reject(new Error(JSON.stringify(message.error))); else resolve(); + } + } + }; + child.stdout.on('data', read); + }); + child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { + protocolVersion: '2025-11-25', capabilities: {}, clientInfo: { name: 'entry-exit-test', version: '1' }, + } }) + '\n'); + await withTimeout(ready, 20000, 'entry-handshake'); + child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n'); +} + +it('production entry exits on client EOF without a client kill fallback', { timeout: 35000 }, async () => { + await entryFixture(async (child, exit, logs) => { + await handshake(child); + child.stdin.end(); + assert.equal(await withTimeout(exit, 9000, 'EOF-exit'), 0, logs()); + assert.match(logs(), /shutting down/); + }); +}); + +it('production entry handles EOF before initialization and does not announce readiness after stop', { timeout: 15000 }, async () => { + await entryFixture(async (child, exit, logs) => { + child.stdin.end(); + assert.equal(await withTimeout(exit, 9000, 'early-EOF-exit'), 0, logs()); + const stopping = logs().indexOf('shutting down'); + assert.ok(stopping >= 0, logs()); + assert.doesNotMatch(logs().slice(stopping), / ready\./); + assert.equal(logs().match(/shutting down/g)?.length, 1); + }); +}); diff --git a/tests/lifecycle-cancellation.test.ts b/tests/lifecycle-cancellation.test.ts index 86918ea..6b2cb3a 100644 --- a/tests/lifecycle-cancellation.test.ts +++ b/tests/lifecycle-cancellation.test.ts @@ -10,9 +10,55 @@ import { Client, InMemoryTransport } from '@modelcontextprotocol/client'; import { getDefaultConfig } from '../src/Core/Config.js'; import { ToolRouter } from '../src/Core/ToolRouter.js'; import { WinCodeMcpServer } from '../src/Gateway/McpServer.js'; -import { ResourceManager, killProcessTree } from '../src/Core/ResourceManager.js'; +import { ResourceManager, killProcessTree, withTimeout } from '../src/Core/ResourceManager.js'; import { checkOperation } from '../src/Core/OperationContext.js'; +it('a stuck adapter cannot prevent other owners and a real child from closing within the shared budget', async t => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wincode-stuck-stop-')); + const config = getDefaultConfig(root); config.timeouts.shutdownMs = 600; + const router = new ToolRouter(config); + const child = cp.spawn(process.execPath, ['-e', 'setInterval(()=>{},1000)'], { windowsHide: true, stdio: 'ignore' }); + const exited = once(child, 'exit'); + router.resources.registerProcess('stuck-stop-fixture', child); + let closed = false; + t.mock.method(router.repomix, 'dispose', () => new Promise(() => {})); + t.mock.method(router.flaui, 'dispose', async () => { closed = true; }); + try { + await assert.rejects(withTimeout(router.dispose(), 2000, 'test-stop'), /resources failed/); + await withTimeout(exited, 1000, 'test-child-exit'); + assert.equal(closed, true); + assert.equal(router.resources.childProcessCount(), 0); + await assert.rejects(router.dispose(), /resources failed/); + } finally { await killProcessTree(child); await fs.rm(root, { recursive: true, force: true }); } +}); + +it('shutdown during initialization prevents later stages and releases late resources', async t => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wincode-init-stop-')); + const config = getDefaultConfig(root); + config.adapters.flaui.enabled = false; + const router = new ToolRouter(config); + let release!: () => void, entered!: () => void, closed = 0, laterStages = 0; + const gate = new Promise(resolve => { release = resolve; }); + const entry = new Promise(resolve => { entered = resolve; }); + t.mock.method(router.repomix, 'initialize', async () => { + entered(); await gate; + router.resources.register('disposable', 'late-init', () => { closed++; }); + }); + t.mock.method(router.text, 'initialize', async () => { laterStages++; }); + try { + const start = router.initialize(); + const rejection = assert.rejects(start, /shutting down|cancelled/i); + await entry; + const stop = router.dispose(); + release(); + await Promise.all([rejection, stop]); + assert.equal(laterStages, 0); + assert.equal(closed, 1); + assert.equal((await router.getRuntimeHealth()).workspaceWatch.active, false); + assert.equal(router.resources.list().length, 0); + } finally { release(); await router.dispose(); await fs.rm(root, { recursive: true, force: true }); } +}); + async function fixture(run: (router: ToolRouter, server: WinCodeMcpServer, client: Client) => Promise) { const root = await fs.mkdtemp(path.join(os.tmpdir(), 'wincode-wp3-')); const config = getDefaultConfig(root); diff --git a/tests/stage1-cleanup.test.ts b/tests/stage1-cleanup.test.ts index ba73df6..aedb922 100644 --- a/tests/stage1-cleanup.test.ts +++ b/tests/stage1-cleanup.test.ts @@ -164,7 +164,7 @@ it('cleanup does not follow an overflow directory junction', async () => isolate } finally { await fs.unlink(link); } })); -it('shutdown waits for the active switch and rejects its queued queries', async () => isolated(async root => { +it('shutdown cancels the active switch before rebinding and rejects its queued queries', async () => isolated(async root => { const router = new ToolRouter(getDefaultConfig(root)); let release!: () => void; let entered!: () => void; @@ -182,8 +182,8 @@ it('shutdown waits for the active switch and rejects its queued queries', async const shutdown = router.dispose(); assert.deepEqual(events, []); release(); - await Promise.all([switching, shutdown, rejected]); - assert.deepEqual(events, ['dispose', 'initialize', 'dispose']); + await Promise.all([assert.rejects(switching, /cancelled/i), shutdown, rejected]); + assert.deepEqual(events, ['dispose']); assert.equal(router.inFlightRequests, 0); assert.equal(router.resources.isDisposed, true); await assert.rejects(router.acquireRequestSlot(), /shutting down/); diff --git a/tools/WinCode.Code.Host/WinCode.Code.Host.csproj b/tools/WinCode.Code.Host/WinCode.Code.Host.csproj index fb4a4c9..13caf12 100644 --- a/tools/WinCode.Code.Host/WinCode.Code.Host.csproj +++ b/tools/WinCode.Code.Host/WinCode.Code.Host.csproj @@ -1,6 +1,6 @@ - 0.13.1 + 0.13.2 Exe net10.0 enable diff --git a/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj b/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj index 01c2339..bd95259 100644 --- a/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj +++ b/tools/WinCode.UIA.Host/WinCode.UIA.Host.csproj @@ -1,7 +1,7 @@ - 0.13.1 + 0.13.2 true Exe net10.0-windows