Files
Ra3ModXmlExt/docs/ai-agent-integration-plan.md
T
2026-09-10 19:18:15 +02:00

1123 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RA3 Mod XML AI Agent 接入计划与进度追踪
> 状态:实施中
> 创建时间:2026-09-09
> 目标:让 AI Agent / 其他工具能够方便、可靠地使用 RA3 Mod XML 扩展构建的语义索引。
---
## 进度追踪
| 阶段 | 内容 | 状态 |
|---|---|---|
| Phase 0 | 计划与进度文件 | ✅ 完成 |
| Phase 1 | 外部快照格式与导出命令 | ✅ 完成 |
| Phase 2 | 查询核心与 CLI | ✅ 完成 |
| Phase 3 | MCP Server | ✅ 完成(基础 stdio MCP |
| Phase 4 | MCP 配置助手 / 稳定 launcher | ✅ 完成(launcher + 启用命令 + Claude/Cursor 配置写入 + 复制配置) |
| Phase 5 | Agent Skill 安装器 | ✅ 完成(默认安装 + 多位置选择 + 安装记录 + 卸载/同步函数) |
| Phase 6 | Live 查询与快照合并策略 | ✅ 完成(本地只读 HTTP live server + endpoint 文件 + MCP 在线优先/离线兜底 + final 索引静默 5 秒自动快照) |
| Phase 7 | 测试、文档、发布 | 🟡 部分完成(README + 计划文档已更新;全部 31 个 test/*.test.mjs 逐个直接运行通过;已用直接 esbuild CLI 验证 dist 构建与 MCP smoke test`node --test` 受沙箱限制,尚未 VSIX 打包/发布) |
### 二期进度(2026-09-10
| 阶段 | 内容 | 状态 |
|---|---|---|
| 二期 A1 | live 响应携带 `projectDir`MCP 侧校验 | ✅ 完成 |
| 二期 A2 | endpoint 按项目分文件(保留全局兜底) | ✅ 完成 |
| 二期 A3 | live server 支持 `?project=` 路由(不再只看活动编辑器) | ✅ 完成 |
| 二期 A4 | 关闭窗口只清理自己写的 endpoint | ✅ 完成 |
| 二期 A5 | `processId` 存活校验 | ✅ 完成 |
| 二期 A6 | live 失败负缓存 | ✅ 完成 |
| 二期 A7 | `snapshotBaseName` 改为 sha1 前缀 + 可读 slug | ✅ 完成 |
| 二期 B1 | Skill 适用范围段落(正/负信号 + `get_status` 探针) | ✅ 完成 |
| 二期 C1 | `get_asset_references`:有界、带 provenance 的正向引用查询 | ✅ 完成 |
| 二期 C2 | 本地 HTTP `/get_asset_references` 与 MCP 工具接入 | ✅ 完成 |
| 二期 C3 | 二期测试与文档 | ✅ 完成(264/264 测试通过) |
### 三期进度(2026-09-10
| 阶段 | 内容 | 状态 |
|---|---|---|
| 三期 0 | 讨论结论与进度落档 | ✅ 完成 |
| 三期 1 | 运行时解析:无 Node 时用 VS Code Electron`ELECTRON_RUN_AS_NODE` | ✅ 完成 |
| 三期 2 | 抽出 `src/agent/liveClient.ts`,MCP 与 CLI 共用转发/校验/负缓存 | ✅ 完成 |
| 三期 3 | `instances/` 注册表 + 跨实例剪枝 + 只读 `index.json` 发现面 | ✅ 完成 |
| 三期 4 | CLI 瘦身:live 优先、快照兜底、cwd 项目识别、补 `projects`/`get_asset_references` | ✅ 完成 |
| 三期 5 | 三期测试与文档 | ✅ 完成(302 用例,299 通过 / 3 因沙箱跳过) |
| 三期 6 | (可选)TCP → 命名管道 / UDS 传输 | ⬜ 未开始 |
---
## 1. 背景与目标
当前 RA3 Mod XML 扩展已经能构建大型项目的语义索引,包括:
- asset 定义:`type / id / file / line / origin / stream`
- `$DEFINE`
- `<Include>` / `xi:include` 关系
- 语义引用和反向引用
- manifest 资产
- `.w3x` art 资产
这些能力目前主要服务于 VS Code 编辑器内部。
本计划的目标是:
1. 让 AI Agent、脚本和其他工具能够方便、可靠地使用该索引;
2. 不需要用户注册全局 CLI / 修改 PATH;
3. 安装/更新扩展后能通过一次性设置完成接入;
4. 在用户持续编辑代码时,索引查询仍然足够实时且不会造成磁盘写放大;
5. 为 AI Agent 提供工具 + 引导 Skill,让它知道何时、如何调用索引。
---
## 2. 核心设计决策
### 2.1 不采用“全局 CLI + PATH”作为主入口
CLI 可以作为高级/调试工具保留,但不是默认路径。
### 2.2 以 MCP Server 作为 AI Agent 主接口
MCP Server 使用本地 stdio 启动,通过绝对路径配置即可,不需要 PATH。
### 2.3 内存索引为主,磁盘快照为辅
- 实时查询尽量走 VS Code 扩展内存中的 `ModIndex`
- 磁盘只保存低频、合并后的“当前稳定快照”作为离线兜底;
- 避免用户连续编辑保存时产生大量磁盘 IO。
### 2.4 Agent Skill 是可选增强层
MCP 注册解决“工具可用”,Skill 解决“Agent 知道怎么用好工具”。
Skill 不作为启用 MCP 的强制条件。
### 2.5 所有查询结果都携带索引状态
外部接口必须返回:索引是否存在、是否正在构建、是否完整、是否 stale、数据版本/时间。
---
## 3. 目标架构
```text
VS Code 扩展
├── 内存 ModIndex
│ │
│ ├── 本地只读查询服务(可选,热数据)
│ └── 低频快照导出(冷数据)
├── MCP 配置助手
│ └── 写入 AI 客户端配置 + 稳定 launcher
└── Agent Skill 安装器
└── 安装到 ~/.agents/skills 等位置
AI Agent / 外部工具
├── MCP Server(推荐)
│ ├── 扩展在线 → 查询内存/本地服务
│ └── 扩展离线 → 读取最近稳定快照
├── CLI(可选)
└── HTTP/JSON-RPC(可选)
```
---
## 4. 组件设计
### 4.1 外部快照格式
定义一份稳定、公开的快照 schema,不直接暴露内部 workspaceStorage 缓存。
```json
{
"schemaVersion": 1,
"project": "D:/Mods/CoronaMod/mods/mods/corona",
"generatedAt": "...",
"buildId": 128,
"phase": "art",
"complete": true,
"stale": false,
"stats": {
"assetCount": 38304,
"referenceCount": 97862,
"defineCount": 240,
"streamCount": 12
},
"assets": [
{
"type": "GameObject",
"id": "...",
"file": "Data/...",
"line": 10,
"origin": "project",
"stream": "static",
"viaInstance": false
}
],
"defines": [],
"references": [],
"includeGraph": [],
"sourceCandidates": []
}
```
生成方式:
-`ModIndex` 导出;
- 使用数组而不是 JS Map
- 原子写入:`temp + rename`
- 同一项目只维护 `current` 快照,不保留每次历史版本;
- 可选维护 `previous` 用于构建期间回退。
### 4.2 MCP Server
MCP Server 不直接持有完整索引,而是作为查询代理:
1. 扩展在线时,转发到 VS Code 扩展的本地只读查询服务;
2. 扩展离线时,读取最近一次稳定快照;
3. 所有工具返回结果时附带索引状态。
建议 MCP 工具集:
```text
get_status()
list_projects()
find_asset(id, type?)
find_references(id, type?)
list_assets_by_type(type, prefix?, limit?)
get_definition(type, id)
resolve_include(source)
is_file_active(path)
find_define(name)
get_usage_guide()
```
### 4.3 稳定 launcher 与 MCP 配置助手
扩展安装路径会随版本变化,所以不能直接写死在 MCP 配置里。
稳定入口:
```text
~/.ra3modxml/
ra3-mod-xml-mcp.cmd
projects.json
snapshots/
current.json.gz
meta.json
skill-install.json
```
MCP 配置示例:
```json
{
"mcpServers": {
"ra3-mod-xml": {
"command": "C:\\Users\\lanyi\\.ra3modxml\\ra3-mod-xml-mcp.cmd",
"args": ["--project", "D:\\Mods\\CoronaMod\\mods\\mods\\corona"]
}
}
}
```
扩展每次激活/升级时刷新 launcher 内容,指向当前扩展安装目录。
这样 MCP 配置只需要写一次,升级不断。
### 4.4 Agent Skill
默认安装:
```text
~/.agents/skills/ra3-mod-xml/
├── SKILL.md
└── references/
└── query-guide.md
```
Skill 内容原则:
- 只描述功能本身;
- 说明“什么情况下该用这个 Skill”以及“怎么用”;
- 不引用各个工作区里的具体文档,例如 `docs/codebase-navigation-guide.md`,避免注意力噪音;
- 保持通用,不绑定某个 mod。
Skill 内容建议包括:
- 索引覆盖范围和状态含义;
- MCP 工具用途;
- 推荐查询工作流:
- 从 ID 找定义 → `find_asset`
- 找所有引用 → `find_references`
- 确认文件是否 active → `is_file_active`
- 追继承链 → `find_references` + `find_asset`
- 找未引用资产 → `unreferenced`
- 使用规则:
- 优先查询索引,而不是全项目 `rg`
- 查询尽量带 type,避免同名不同类型混淆;
- 不要把整个索引快照读进上下文;
- 如果索引状态不是 ready/stale,应查询状态或重试。
### 4.5 索引状态与一致性
定义索引状态:
| 状态 | 含义 |
|---|---|
| `no_index` | 从未构建 |
| `building` | 正在构建/重建 |
| `ready_xml` | XML phase 已发布,art 未完成 |
| `ready` | 完整 final 索引 |
| `stale` | 构建期间有文件变化 |
| `error` | 构建失败但保留 last good |
查询接口支持两种模式:
- 默认 / eventual:有可用快照就返回,同时附带 `stale/incomplete` 标记;
- 严格 / consistent:调用方传 `wait_for_index=true` / `allow_stale=false`,等待新 final 索引或超时。
### 4.6 持久化策略
- 热数据:内存 `ModIndex`
- 实时查询:本地查询服务 / MCP 转发;
- 冷数据:低频快照,例如:
- 用户手动导出;
- 索引稳定一段时间后自动写一次;
- 扩展退出前写一次(尽力而为);
- 不每次 rebuild 都写全量快照。
### 4.7 用户交互流程
安装/更新扩展后:
1. 首次索引完成;
2. 如果尚未启用且未被用户忽略,弹出提示:
```text
RA3 Mod XML 索引已就绪。
是否让 AI Agent 使用索引查询能力?
```
3. 用户点击“启用”;
4. 扩展默认注册 MCP Server
5. 同时询问/默认勾选:
```text
[✓] 同时安装 RA3 Mod XML Agent Skill(推荐)
```
6. 写入 MCP 配置;
7. 安装 Skill 到 `~/.agents/skills/ra3-mod-xml/`
8. 提供“安装到其他位置”或“手动操作指南”。
### 4.8 安全设计
- 本地查询服务只监听 `127.0.0.1`
- 使用随机 token
- MCP Server 默认只读,不提供文件修改/命令执行工具;
- 写 MCP 配置前必须用户同意;
- Skill 安装目录记录 managed marker,避免覆盖用户自定义内容。
---
## 5. 实施阶段
### Phase 1:外部快照格式与导出命令
- 新增 `src/agent/snapshot.ts`
- 新增命令:`ra3modxml.exportIndexSnapshot`
- 定义 `schemaVersion`
-`ModIndex` 生成可序列化快照
- 原子写入 `~/.ra3modxml/snapshots/current.json.gz`
- 输出状态元数据
### Phase 2:查询核心与 CLI
- 复用纯 TS `indexer` 模块;
- 新增 `src/agent/query.ts`
- `findAsset`
- `findReferences`
- `isFileActive`
- `listAssetsByType`
- `getStatus`
- 新增 CLI 入口(可选):
- 不注册 PATH
- 通过绝对路径调用;
- 供脚本/调试使用。
### Phase 3MCP Server
- 新增 `src/agent/mcpServer.ts`
- 支持 stdio 协议;
- 启动时读取连接信息/快照;
- 提供前述 MCP 工具;
- 每个响应带索引状态。
### Phase 4MCP 配置助手
- 新增 `src/agent/setup.ts`
- 创建稳定 launcher
- 检测/写入常见客户端配置:
- Claude Desktop
- Cursor
- Codex
- 通用 JSON
- 提供“复制 MCP 配置”;
- 提供手动操作指南。
### Phase 5Agent Skill 安装器
- 新增 `src/agent/skill.ts`
- 生成 `SKILL.md` + `references/query-guide.md`
- 默认安装到 `~/.agents/skills/ra3-mod-xml/`
- 可选安装到:
- `~/.claude/skills/ra3-mod-xml/`
- 项目 `.agents/skills/`
- 项目 `.claude/skills/`
- 自定义位置
- 记录已安装位置,扩展升级时自动同步;
- 不覆盖用户自定义内容。
### Phase 6Live 查询与快照合并
- 在扩展内启动本地只读查询服务;
- MCP Server 在线时优先转发到扩展;
- 离线时读取 `current.json.gz`
- 实现快照 quiet-period 合并写入,避免频繁全量落盘;
- 实现状态机:`no_index / building / ready_xml / ready / stale / error`
### Phase 7:测试、文档、发布
- 测试快照导出/导入一致性;
- 测试 MCP 工具查询;
- 测试 Skill 安装/更新/卸载;
- 测试持续编辑场景下的状态与 stale 行为;
- 更新 README / docs
- 发布新版本。
---
## 6. 预估文件改动
### 新增文件
```text
src/agent/
types.ts
snapshot.ts
query.ts
mcpServer.ts
setup.ts
skill.ts
localServer.ts
launcher.ts
docs/
ai-agent-integration-plan.md
```
### 修改文件
```text
package.json # commands / configuration
src/extension.ts # 注册命令、设置、初始化
src/workspace.ts # 暴露 index status / snapshot 发布钩子
src/features/agentSetup.ts # 用户引导 UI(或并入 agent/setup.ts
```
---
## 7. 验收标准
1. 用户安装/更新扩展并完成一次索引后,可以看到“启用 AI Agent 访问”的引导。
2. 用户点击启用后,不需要手动配置 PATH,不需要手动查找 workspaceStorage。
3. 常见 AI 客户端能通过 MCP 调用索引工具。
4. Agent 能正确回答:
- “某 asset 定义在哪个文件?”
- “谁引用了这个 asset?”
- “这个 XML 文件是否真的被 Include?”
5. 查询接口在无索引/索引更新中不会返回误导性空结果。
6. 用户连续编辑保存时,不会因为频繁快照写入而产生明显卡顿或磁盘膨胀。
7. Skill 安装后,Agent 知道优先使用 MCP 查询,而不是全文 grep。
8. 扩展升级后,MCP 配置仍然有效,Skill 能自动同步到已安装位置。
---
---
# 第二部分:二期计划(讨论结论 + 实施)
> 讨论时间:2026-09-10
> 主题:Skill 的项目识别边界、正向/间接引用查询的取舍、MCP 与 localhost 架构现状
---
## 8. 讨论结论一:谁来判定"这是不是一个红警 3 模组项目"
### 问题
Skill 需要说明"何时应该被使用"和"适用范围"。这要求 Agent 能理解什么是
"红警 3 模组项目里的 XML",同时避免把无关项目误判为 RA3 模组。
问题在于:这个判断应该由扩展/Skill 自己解释(甚至给出推断规则),还是应该
由项目开发者在自己的项目文档 / `AGENTS.md` 里声明?
### 结论:分层,且 Skill 不做复杂推断
| 层 | 职责 | 理由 |
|---|---|---|
| **Skill** | 说明能力覆盖范围 + 少量正/负信号 + 便宜的探测方式 | Skill 是用户全局的,会在**所有**对话里加载,必须简短且能自我限制 |
| **项目开发者**`AGENTS.md` / README / 项目标记) | 声明"这个仓库是什么项目、要不要用这些工具" | 只有项目自己知道自己是什么,这是每个仓库的局部事实 |
| **MCP 工具自身** | 用响应证明自己是否可用 | `get_status` 是最便宜的判定,不应靠读文档猜 |
### 为什么不把判断规则写进 Skill
- Skill 在 `~/.agents/skills/` 下全局生效;如果写一堆判定规则,会在与 RA3
无关的项目里制造"要不要用这个工具"的注意力噪音。
- 规则永远不完备。真实反例:
- 目录里有 `Schemas/xsd/CnC3Types.xsd` → 可能只是 SDK 本身,不是 mod;
- 只有一堆从 SDK 抄来的 `.xsd` → 是"RA3 相关",但没有 mod 数据;
- `CnC3Types.xsd` 里的 `CnC3`**C&C3**(命令与征服 3 / 凯恩之怒),
所以"存在 CnC3Types.xsd" **不等于** "是 RA3 模组"
-`Data/Mod.xml` / `*.babproj` / `Data/additionalmaps/mapmetadata_*.xml`
→ 基本可确认是 **SAGE / BinaryAssetBuilder** 系项目(RA3 只是其中之一)。
- 一旦误判,Agent 会拿 RA3 的索引数据去回答另一个项目的问题,比不使用更糟。
### 落地方案
**Skill 侧**`src/agent/skill.ts``SKILL_MD`)新增 applicability 段落:
- 正向信号(任一命中即可尝试):
`Data/Mod.xml``Data/additionalmaps/mapmetadata_*.xml``*.babproj`
XML 根为 `<AssetDeclaration>`;使用 `<Includes><Include source="DATA:…"/></Includes>`
- 明确否定:无关仓库、仅含拷贝 `.xsd` 的项目、通用 XML 配置、构建脚本、
非 SAGE 游戏项目。
- 便宜的探针:不确定时先调 `get_status`;它返回索引归属的 `projectDir`
如果与当前工作区根不一致,或状态是 `no_index`,就**停止使用索引工具**
改为直接读文件。
这样即使误触发,成本也只是一次 `get_status`,不会污染后续推理。
**项目开发者侧**:在项目根 `AGENTS.md` 写一句即可,例如:
```markdown
## RA3 Mod tooling
This repository is a Red Alert 3 mod (SAGE XML). When locating asset
definitions or references, prefer the `ra3-mod-xml` MCP tools
(`find_asset`, `find_references`, `is_file_active`) over full-text search.
```
扩展**不自动**往用户仓库写标记文件(如 `.agents/ra3-mod-xml-project.json`
作为二期内容;若将来提供,必须显式确认。
---
## 9. 讨论结论二:要不要提供"某个 Asset 直接/间接引用的所有 Asset"
### 场景
用户要求 Agent"查阅或修改雅典娜炮的武器"。Agent 需要:
1. 找到雅典娜炮的 GameObject
2. 读源码,发现 `<FireWeaponUpdate />` / `<WeaponSetUpdate />` 等子元素,
在这些子元素里找到武器引用;
3.`<CreateObjectDie />` 引用了临时物体,还要去读临时物体的 GameObject
在它的 `FireWeaponUpdate` 里找武器;
4. 若雅典娜炮只有 `FireWeaponUpdate``WeaponSetUpdate`
`inheritFrom``BaseCannon` 上,还要读 `BaseCannon` 并综合判断。
评估问题:
- 是否有必要提供"某个 Asset 直接或间接引用的所有 Asset"
- 只给精炼 asset 列表,Agent 不知道是哪个子元素导致的引用,有用吗?
- 若同时提供上下文,磁盘/内存/性能允许吗?内容长度会不会反而比直接读源码更贵?
- 相对直接读源码未必有显著提升,反而增加注意力噪音?
- 间接引用会不会多项式/指数爆炸?
### 先指出当前实现的真实缺口
现在的索引只有**反向**引用(谁引用了我),没有**正向**查询(我引用了谁)。
`find_references(AthenaCannon)` 返回的是"谁引用了雅典娜炮",而不是"雅典娜炮
用了哪些武器"。所以上述 Agent 工作流**目前完全无法用工具完成**,只能读源码。
因此问题不是"要不要锦上添花",而是这里确实缺一个基础能力。
### 结论:不提供"所有可达 Asset 的扁平闭包",改提供"有界 + 带 provenance 的边列表"
**不采用扁平静态闭包的理由:**
1. **丢失 provenanceAgent 仍然要读源码。** 到底是 `FireWeaponUpdate` 的武器、
`WeaponSetUpdate` 的武器、`CreateObjectDie` 临时物体的武器,还是
`inheritFrom` 继承来的武器?这决定了 Agent 改哪个文件、加不加
`xai:joinAction`。丢掉上下文,列表基本没用。
2. **尺寸会失控。** 一个 GameObject 的可达闭包可能是
`Weapon → Projectile → Warhead → DamageNugget → FX → Particle → Texture`
`Model → Mesh → Material → Texture``AttributeModifier``Upgrade``Command`
`Button`……Corona 规模下单个单位几百到上千节点完全可能。200 条边就是几万
token**比直接读 3 个小 XML 文件贵得多**。
3. **注意力噪音。** 列表里 80% 是与当前问题无关的 Texture / FX 时,推理质量下降。
4. **"间接"的诱惑会持续膨胀。** 一旦提供无界闭包,Agent 会习惯性调用它,
然后被淹没。
**但以下特性确实值得工具化:**
- 多跳遍历是**纯机械**的,正是工具该做的事;
- `inheritFrom` 链 + `xai:joinAction` 语义很微妙,Agent 手推容易错;
- 每跳都 read 一个文件,Corona 下成本和 token 都很高;
- 工具可以做到**确定性、不遗漏**(按 XSD 判定"哪些属性是引用"
而不是靠 Agent 逐个注意)。
**设计要点(二期实现遵循):**
- 返回**边列表**而不是节点集合。每条边携带:
- `from`effective asset)、`to`(解析出的定义位置)、
- `via.kind``attribute` / `content` / `inheritFrom`)、
- `via.element``via.parent``via.attribute`
- `source.file` / `source.line`XML 实际所在位置),
- `definedIn`(当该边来自 `inheritFrom` 祖先的 XML 时,标明真正写它的资产)。
- **默认 `depth: 1`,硬上限 3。** Agent 拿到直接引用后自己决定是否对某个具体
节点递归,比一次性 dump 闭包更省 token、更有针对性。
- **`targetTypes` 过滤**是抑制噪音最有效的手段。雅典娜炮场景就是
`targetTypes: ["WeaponTemplate"]`(可选再带 `GameObject``CreateObjectDie`),
返回可能只有 310 条边。
- **`inheritFrom` 不消耗 depth**:继承是资产自身定义的一部分,不是运行时引用。
因此在同一 depth 内递归遍历继承链(链长上限 8),并把 `definedIn` 标出来。
- **截断时返回 `omittedByTargetType` 摘要**,让 Agent 知道被砍掉了什么形状的数据,
可以精确下钻,而不是无脑重试或放弃。
- **不提供"合并后的有效值"**`joinAction` 的 Replace/Remove/merge 语义算错的风险
太高,一期不做。
**关于爆炸和成本:**
- 算法上不是指数:带 visited-set 的 BFS 是 `O(V+E)`。真正的风险是**语义宽度**
不是复杂度,因此用 `depth` + `targetTypes` + `maxEdges` 三重限制。
- **只做 live 查询**(VS Code 打开时可用)。要拿元素上下文,最干净的做法是按需
解析定义所在文件的 DOM,而不是往 records 缓存里塞元素名/属性名 —— Corona 有
~98k 条引用记录,再塞 5 个字符串字段会让常驻内存明显膨胀,为一个默认 `depth=1`
的功能付出这个成本不划算。
- 离线快照路径明确返回"需要 live index",而不是静默返回空结果。
---
## 10. 讨论结论三:MCP / localhost 的实际架构与多窗口缺陷
### 当前架构(事实)
```text
AI 客户端
│ stdio (JSON-RPC, MCP)
dist/agent/mcpServer.js ← 由 AI 客户端启动,参数 --project <dir>
│ HTTP (127.0.0.1:临时端口, Bearer token)
VS Code 扩展进程内的 localServer ← 仅在"启用 AI Agent 访问"后启动
├─ 在线 → ws.activeIndex() 实时内存索引
└─ 离线 → ~/.ra3modxml/snapshots/project-<hash>.json.gz
```
- **AI 客户端不需要知道端口。** MCP Server 自己读
`~/.ra3modxml/endpoint.json``url` + `token``fetch`。localhost 层是
纯实现细节。
- **端口**`server.listen(port ?? 0, "127.0.0.1")`,即由操作系统分配临时端口,
不会冲突;当前未传 `port`,所以永远是临时端口。
- **鉴权**`Authorization: Bearer <token>`token 为启动时随机生成;绑定
`127.0.0.1`,局域网访问不到。
- **降级**live 查询失败(endpoint 不存在 / 连接被拒 / 超时 1.5s)时回退到快照。
### 多项目 / 多 VSCode 窗口的真实缺陷
`endpoint.json` 是**全局唯一**的一个文件,所有 VSCode 窗口都往它写入:
1. **最后启用的窗口覆盖前面所有窗口。** 两个窗口都启用了 Agent 访问时,
只有后启动的那个能被 MCP Server 找到。
2. **可能串项目(严重)。** 假设窗口 B 覆盖了 endpoint,而某个 MCP Server 是用
`--project 窗口A` 启动的,它的 `tryLiveQuery` 会打到**窗口 B** 的 server
拿到窗口 B 的索引数据,而返回值里没有任何东西能暴露这个不一致。
这比 `no_index` 危险得多 —— 会静默给出错误答案。
3. **关闭任意窗口都会 `clearEndpoint()`**,把还开着的其他窗口的 live 通道一起断掉。
4. **同一窗口内多项目也不稳。** live server 用
`getIndex: () => ws.activeIndex()`,而 `activeIndex()` 跟着**当前活动编辑器**走。
用户切到另一个项目的文件,live 查询就会静默换项目;而 `endpoint.json` 里的
`projectDir` 只在启动时写一次,之后不更新。
5. `processId` 虽然写进了 endpoint,但**从未被校验**VSCode 崩溃后残留的 endpoint
只能靠连接失败兜底。
6. 离线快照路径是**正确**的 —— `snapshotPathForProject()` 按项目路径做 hash
多项目互不干扰。问题只在 live 层。
### 修复方案(二期 A,按性价比排序)
1. **live 响应必须带 `projectDir`MCP 侧必须校验它和 `--project` 一致**
不一致就直接走快照。这是最小的正确性补丁,最先做。
2. **endpoint 按项目存**`~/.ra3modxml/endpoints/<project-hash>.json`
MCP Server 按 `--project` 找对应的那个;同时保留 `endpoint.json` 作为
"最近"的兼容入口。
3. **live server 支持 `?project=` 路由**,而不是只认 `activeIndex()`
扩展侧加 `indexForProject(projectDir)`,一个窗口的一个 server 就能服务它
所有已索引项目,也就不怕用户切编辑器。
4. **关闭时只清自己的那一条**,不无条件删全局 endpoint。
5. **校验 `processId` 是否存活**,不存活就当 endpoint 失效。
6. `tryLiveQuery` 失败后做几秒的"live 不可用"负缓存,避免每次调用都试一次。
7. `snapshotBaseName` 换成 sha1 前缀 + 可读 slug(与 `diskCacheKey` 一致的做法),
降低碰撞概率。
---
## 11. 二期实施清单
### 二期 A:多窗口 / 多项目正确性
- `src/agent/snapshot.ts`:新增 `projectHash()``snapshotBaseName()` 改为
`<slug>-<sha1-12>`
- `src/agent/endpoint.ts`:新增
`endpointPathForProject()` / `writeEndpointForProject()` /
`readEndpointForProject()` / `clearEndpointForProject()` / `isProcessAlive()`
- `src/workspace.ts`:新增 `indexForProject(projectDir)`
- `src/agent/liveQuery.ts``liveStatus()` 在没有索引时回落到请求的 `projectDir`
- `src/agent/localServer.ts``getIndex(projectDir?)` 路由、异步 handler、
`/projects` 端点、所有响应携带 `index.projectDir`
- `src/agent/mcpServer.ts`:按项目读 endpoint、校验 `projectDir``processId`
`?project=` 透传、live 失败负缓存、响应 `projectDir` 二次校验。
- `src/extension.ts`:启动时按项目写 endpoint、关闭时只清自己写过的那些。
### 二期 BSkill 适用范围
- `src/agent/skill.ts``SKILL_MD` 增加 applicability 段落(正向信号、明确否定、
`get_status` 探针);保持"不引用工作区文档"的原则。
### 二期 C`get_asset_references`
- `src/agent/forwardRefs.ts`(新增):DOM-based、有界、带 provenance 的正向引用遍历。
- `src/agent/localServer.ts`:新增 `GET /get_asset_references`
- `src/agent/mcpServer.ts`:新增 `get_asset_references` 工具(live-only
离线时返回明确的"需要 live index"说明)。
- `src/extension.ts`:为 live server 提供 `loadFile()`(读文本 + 解析 + LineMap)。
### 二期 D:测试
- `test/agentEndpoint.test.mjs`:按项目 endpoint、进程存活、清理隔离。
- `test/agentLocalServer.test.mjs``?project=` 路由、`projectDir` 校验、
`/get_asset_references`
- `test/agentForwardRefs.test.mjs`:直接引用、继承链、`CreateObjectDie` 内容引用、
`targetTypes` 过滤、`maxEdges` 截断、`depth` 上限。
- `test/agentSkill.test.mjs`Skill 文本包含 applicability 段落且不含工作区文档引用。
- `test/agentMcpRouting.test.mjs`live URL 固定 `project`、跨项目响应拒绝。
---
## 12. 二期实施结果
全部完成,`npx tsc --noEmit` 通过,`test/*.test.mjs`**264/264** 通过。
### 新增 / 修改文件
```text
新增:
src/agent/forwardRefs.ts 有界、带 provenance 的正向引用遍历
test/agentForwardRefs.test.mjs
test/agentMcpRouting.test.mjs
修改:
src/agent/endpoint.ts 按项目 endpoint + isProcessAlive()
src/agent/snapshot.ts projectHash() / projectSlug() / snapshotBaseName()
src/agent/liveQuery.ts liveStatus() 回落请求的 projectDir
src/agent/localServer.ts 异步 handler、?project= 路由、/projects、/get_asset_references
src/agent/mcpServer.ts 按项目 endpoint、projectDir 校验、负缓存、get_asset_references
src/agent/skill.ts applicability 段落 + get_asset_references 用法
src/workspace.ts 新增 indexForProject()
src/extension.ts 按项目写/清 endpoint、loadFile、endpoint 刷新
test/agentEndpoint.test.mjs
test/agentLocalServer.test.mjs
test/agentSkill.test.mjs
```
### 关键行为
**多窗口 / 多项目**
- `~/.ra3modxml/endpoints/<slug>-<sha1-12>.json` 一项目一文件;MCP Server 先读自己项目的
文件,读不到才退回全局 `endpoint.json`,且仅在 `projectDir` 匹配时使用。
- 每个 live 请求带 `?project=`server 用 `ModWorkspace.indexForProject()` 路由,
不再依赖"当前活动编辑器"。
- MCP 侧对响应里的 `index.projectDir` 做二次校验,不匹配就拒答并提示重新启用,
而不是给一个看起来合理但属于别的项目的答案。
- 关闭窗口只删除本窗口写过的 endpoint 文件。
- `processId` 不存活时 endpoint 视为失效。
- live 连接失败后有 5 秒负缓存,避免每次工具调用都探一次。
**Skill**
- 新增 `When this skill applies` 段落:正向信号(`Data/Mod.xml`
`mapmetadata_*.xml``*.babproj``<AssetDeclaration>`
`uri:ea.com:eala:asset`)、明确否定规则(无关仓库、仅含拷贝 `.xsd` 的项目)、
以及 `CnC3Types.xsd` 属于 C&C3 而非 RA3 的反例。
- 规定用 `get_status` 做便宜探针:状态为 `no_index``projectDir` 不匹配时
停止使用索引工具,改为直接读文件。
- 仍然不引用任何工作区文档(有测试断言)。
**`get_asset_references`(正向引用)**
- 返回**边**`from` / `to` / `via{kind,element,parent,attribute}` /
`source{file,line,character}` / `definedIn` / `value`
- `depth` 默认 1、上限 3`inheritFrom` 不消耗 depth,祖先 XML 在同层遍历,
`definedIn` 标出真正写这条边的资产(解决"雅典娜炮自己没有 WeaponSetUpdate
但 BaseCannon 有"的场景)。
- `targetTypes` 按 XSD 可赋值性过滤,但 `inheritFrom` 边始终保留,因为
它解释了其余边写在哪里。
- `maxEdges` 截断时返回 `truncated` + `omittedByTargetType`(只统计因上限被丢弃的边,
已被 `targetTypes` 过滤的不计入)。
- 只走 live 路径;不可用时返回明确的错误说明,而不是空结果。
### 已知限制
- `get_asset_references` 不计算 `xai:joinAction``Replace`/`Remove`)合并后的
有效值,需要调用方自行确认。
- 只有 live 路径支持正向引用;离线快照路径明确返回"需要 live index"。
- `snapshotBaseName` 改为 `<slug>-<sha1-12>`,旧的 `project-<hash>.json.gz`
快照文件不会被自动清理(该功能尚未发布,无兼容负担)。
- 本沙箱禁止 spawn 子进程,因此 `node --test``npm run build` 无法直接运行;
验证方式为:逐个 `node test/*.test.mjs`、直接调用 esbuild CLI 构建 dist、
用管道驱动 `dist/agent/mcpServer.js` 做端到端 smoke test。
---
---
# 第三部分:三期计划(讨论结论 + 实施)
> 讨论时间:2026-09-10
> 主题:Agent 自发现 MCP 的现实边界、崩溃残留清理、CLI 定位、无 Node 环境
---
## 13. 讨论结论:自操作 Agent 能否自行发现并配置 MCP
### 现状(核实过代码)
`SKILL_MD` 全文不含 `~/.ra3modxml`、launcher 路径、CLI 或任何"工具不存在时怎么办"
的说明。它的第一条指令是"调用 `get_status`"。因此当 skill 已加载但 MCP 工具
不在工具列表里时,agent 只能得到 unknown tool,然后没有下一步。
三个独立缺口:
1. **没有发现面。** 磁盘上的文件名带 `sha1-12` 哈希与 slug
`endpoints/<slug>-<sha1-12>.json``snapshots/<slug>-<sha1-12>.json.gz`),
只有读过本仓库源码的人才知道这套命名。agent 没有"读一个文件就知道全貌"的入口。
2. **skill 与 MCP 是独立安装的。** `installAgentSkill` 装的是
`~/.agents/skills/ra3-mod-xml/`,与 MCP 配置无关,所以"skill 在、MCP 不在"
是常见状态,而这恰恰是最需要 fallback 的场景。
3. **多数 harness 不允许 agent 给自己挂 MCP。** Claude Code 需要重启才能加载新的
MCP server(相关 issue 与 SIGHUP workaround 见调研),Reddit 上也有"每次装
MCP 都要手动重启"的反馈;反例是 LibreChat 的设置面板明确"take effect without
a restart"。所以这是 **harness 相关**:agent 可以**写**配置,但通常不能让自己
**当前会话**用上,只能为下一次会话准备好。
### 结论
- **skill 不应假设 MCP 已就绪**,而要给出"如何触达索引"的阶梯:
1. MCP 工具已在工具列表 → 直接用;
2. 否则读发现面清单;不存在 → 告诉用户去 VS Code 跑一次 Enable**不要瞎猜**
存在 → 用 shell 走 stdio,或用 CLI
3. 若能写自己的 MCP 配置,可以提议代写,但必须告知**下个会话才生效**。
- **"需要重启才生效"这类细节不必写进 skill。** 新一代自操作 agent 本来就该自己
探测能力边界;skill 只需提示它"检查自己的能力范围并告知用户"。
- **不要把各 harness 的配置路径写进 skill 文本。** 那些路径会变,写进 skill 等于
把易变知识固化进 agent 上下文。逻辑留在 `setup.ts`
(已有 `addMcpServerToConfigFile` / `commonMcpConfigTargets`),通过 CLI 暴露成
`configure --harness <name>`skill 只说"在用户同意后可以运行这条命令"。
---
## 14. 讨论结论:崩溃残留与 `instances/` 注册表
### 问题确认
核实结果:全仓只有两处 `rm`,都在 `stopAgentLocalServer`(即优雅 dispose)中。
`endpoints/` **没有任何剪枝逻辑**。因此:
- **崩溃后文件永久残留**,只有下次打开**同一个工作区**才会覆盖同名文件;
删掉项目、移动目录、改 SDK 路径后,旧文件永远躺着。
- **PID 复用是真实风险。** Windows PID 会被回收;若残留 PID 恰好被无关进程占用,
`isProcessAlive` 返回 true → 连接失败 → 5 秒负缓存 → 反复重试。不会给出错误
答案(`responseProjectMismatch` 仍在),但会持续做无用功。
### 结论:不要"共享一个 JSON + 加锁合并"
共享单文件需要锁文件(Windows 上 `wx` + 退避 + 陈旧锁窃取)、需要
read-modify-write 合并(否则并发写互相丢条目),而锁本身也会被崩溃留下
——**为了修 A 的崩溃问题引入了 B 的崩溃问题**。
改用**写入方互不重叠**的结构:
```text
~/.ra3modxml/
instances/
vscode-<pid>-<rand>.json ← 每个 VS Code 窗口一个,只有属主会写
index.json ← 只读合并视图,供 agent/人发现
snapshots/<slug>-<sha1-12>.json.gz
skill-install.json
```
- **无锁、无合并**:每实例只写自己那一个文件,天然无竞争。
- **崩溃自愈**:任何实例激活时扫一遍 `instances/``isProcessAlive(pid) === false`
的直接 unlink,不需要等"下次打开同一个工作区"。
- **读方仍要校验**liveness = PID 存活 **且** 能连上;PID 复用场景下连接失败即
视为死,并顺带触发剪枝。
- `index.json` 是纯只读派生物(由 `instances/` 生成),机器协调文件与人类/agent
发现面分离。
`endpoints/` 保留为兼容读取路径,不再主动写入。
---
## 15. 讨论结论:CLI 定位与"稳定 IPC"
### 澄清:端口从未写进 harness 永久设置
写进配置的是 launcher
```json
{ "mcpServers": { "ra3-mod-xml": { "command": "~/.ra3modxml/ra3-mod-xml-mcp.cmd", "args": ["--project", "..."] } } }
```
端口由 `instances/*.json` 桥接,每次开新会话时由 MCP server 现读。所以
"临时端口会变"对 harness 完全透明。换命名管道/UDS 的收益不是"稳定性",而是:
- Windows 防火墙对监听 TCP socket 可能弹窗,命名管道不会;
- 不占端口;
- 权限模型更自然(管道可设 ACL;TCP loopback 任何本机进程都能连,现在靠 token)。
代价:Node 的 `fetch` 不支持 socketPath,客户端要从 `fetch` 换成
`http.request({ socketPath })`(服务端 `listen(pipePath)` 即可,路由不用改)。
### CLI 定位:薄
CLI 只做两件事:**读磁盘快照** + **转发给扩展(live**。不要在 CLI 里重新实现
"高级功能",那等于写两遍。
**但要注意**:转发逻辑现在锁死在 `mcpServer.ts`
`tryLiveQuery` / `liveUrlForTool` / `responseProjectMismatch` / 负缓存)。
若 CLI 再写一遍就是重复。因此必须先抽 `src/agent/liveClient.ts`,让
`mcpServer.ts``cli.ts` **同时**依赖它:
- 换传输层时只改一个文件;
- `projectDir` 校验、负缓存等安全逻辑不会在两个入口之间漂移。
---
## 16. 讨论结论:无 Node 环境(已验证)
### 问题
- 不能假定用户装了 Node。
- `writeLauncher` 生成的是裸 `node "<server>" ...`,所以**没装 Node 的用户
点完 Enable 之后 MCP 起不来**,而且不会报"缺 node"。这是**已存在的缺口**
不是未来风险。
- 打包自包含或原生二进制是否必要?
### 结论:不需要打包,VS Code 自带运行时,只是还没用它
VS Code 的 Electron 二进制可以当 Node 用:
```bat
@echo off
set ELECTRON_RUN_AS_NODE=1
"C:\...\Microsoft VS Code\Code.exe" "<ext>\dist\agent\mcpServer.js" --project "D:\..."
```
**本机实测(2026-09-10VS Code 1.135.0 / Electron 42.8.1**
| 项目 | 结果 |
|---|---|
| `ELECTRON_RUN_AS_NODE=1` 生效 | ✅ `process.versions.electron = 42.8.1` |
| Node 版本 | `24.18.1`(系统 Node 为 24.16.0 |
| 必需模块(`fs`/`fs/promises`/`path`/`os`/`http`/`readline`/`zlib`/`crypto`/`util`/`net`/`child_process` | ✅ 全部可载入 |
| `node:test` | ✅ 可载入 |
| `forwardRefs.parseLoadedXml` | ✅ `elements=2` |
| `localServer.startLocalServer` + HTTP 请求 | ✅ 正常返回 |
| `zlib` gzip/gunzip 往返 | ✅ |
| **打包后的 `dist/agent/mcpServer.js` 完整 stdio 会话** | ✅ `initialize` / `tools/list`(9 个工具) / `tools/call get_status` 全部正确 |
因此:
- **零外部依赖**,只装 VS Code 就够;
- 路径来自 `process.execPath`,自动适配 Code / Insiders / VSCodium / 各平台;
- VS Code 升级后路径变了也没关系——扩展每次激活重写 launcher,而 launcher 本身
`~/.ra3modxml/` 下路径稳定;
- 需要 fallbackElectron 二进制被移动/卸载时回退到 PATH 上的 `node`
已知代价(实测未测,需留意的边界):
- Electron 以 Node 模式启动比真 Node 慢(握手延迟增加,对 MCP 应无碍);
- 远程/WSL/容器场景下 `process.execPath` 是 server 端二进制,MCP 必须跑在远端
—— 独立边界问题,本期不解决。
### 如何在"本机有 Node"的前提下测试"无 Node"
不需要真的卸载 Node。要做的是**证明 Electron 路径可以独立工作**,即整条链路
不依赖"PATH 上能解析到 `node`"
1. 直接只用 Electron 二进制驱动 `dist/agent/mcpServer.js`(已做,见上表);
2. 让解析出的 launcher **只用** Electron,不回落;
3. 断言 launcher 文本中不出现裸 `node`(当 Electron 可用时)。
这三点都在本环境可做,无需移除 Node。
---
## 17. 三期实施清单
| 优先级 | 内容 | 理由 |
|---|---|---|
| 1 | 运行时解析(`src/agent/runtime.ts`+ launcher 优先 Electron | 现在是"配好了但可能起不来",最伤 |
| 2 | 抽 `src/agent/liveClient.ts` | 后续所有传输层改动的前提;避免转发逻辑写两遍 |
| 3 | `instances/` + 跨实例剪枝 + 只读 `index.json` | 修崩溃残留,兼做 agent 发现面 |
| 4 | CLI 瘦身(live 优先 / 快照兜底 / cwd 识别 / `projects` | 让"没有 MCP 的 agent"也能用 |
| 5 | 传输层 TCP → 命名管道 / UDS | 收益中等,需 2 完成,可缓 |
---
## 18. 三期实施结果
全部完成。
### 新增 / 修改文件
```text
新增:
src/agent/runtime.ts 运行时解析 + launcher 脚本生成(纯函数,可测)
src/agent/liveClient.ts 共享 live 转发层(MCP 与 CLI 共用)
src/agent/instances.ts instances/ 注册表、剪枝、只读 index.json
test/agentRuntime.test.mjs
test/agentLiveClient.test.mjs
test/agentInstances.test.mjs
test/agentCli.test.mjs
test/agentLiveE2E.test.mjs
tools/probe-electron-node.cjs Electron-as-Node 能力探针(保留为证据)
tools/serve-fake-index.cjs 假 live 索引服务器(手工 smoke 用)
修改:
src/agent/setup.ts writeLauncher 改用 runtime + launcherScript
返回 { path, runtime, nodeFree }
src/agent/mcpServer.ts 改用 LiveClient;新增 list_projects
启动时剪枝崩溃实例;require.main 守卫
src/agent/cli.ts 重写为薄客户端:live 优先 / 快照兜底 /
cwd 项目识别 / projects / outgoing
src/extension.ts instances/ 写入与剪枝、index.json 刷新、
launcher 激活时刷新、nodeFree 警告
```
### 关键行为
**无 Node 运行(三期 1**
- launcher 优先用 VS Code 的 Electron 二进制(`ELECTRON_RUN_AS_NODE=1`),
Node 只作为"Electron 二进制不存在"时的兜底:
`if not exist "%RA3_RUNTIME%" goto :ra3_node`
-`goto` 而不是 `if (...)` 块:批处理里块内的 `%errorlevel%` 在**解析时**展开,
会导致退出码错误。
- 扩展激活时会重写 launcher(不只是 Enable 时),所以 VS Code 升级换了路径
也能自动跟上。
- 解析不到 Electron 时会警告用户"launcher 将依赖 PATH 上的 Node"。
**`instances/` 注册表(三期 3**
```text
~/.ra3modxml/
instances/vscode-<pid>-<rand>.json 每窗口一个,只有属主写
index.json 只读合并视图(不含 token)
endpoints/<slug>-<sha1-12>.json 兼容读取,不再主动写
snapshots/…
skill-install.json
```
- **无锁无合并**:写入方互不重叠,没有 read-modify-write 竞争。
- **崩溃自愈**:任何实例启动时剪掉 PID 已死的条目,不需要等同一工作区重开。
- **只剪确定的死 PID**`isProcessAlive` 对未知/缺失 PID 返回 true
所以老格式文件不会被误删。
- `index.json` 是派生物,只用于发现;断言过其中不含 token。
**共享 live 层(三期 2**
`liveClient.ts` 现在同时被 MCP 与 CLI 使用,集中了:
- 请求永远带 `?project=`(否则会被活动编辑器的项目回答);
- `index.projectDir` 不匹配就拒答;
- 死 PID 视为失效;
- 失败后 5 秒负缓存(可注入时钟,测试可确定性验证);
- 端点发现顺序:按项目文件 → 任何 `projects` 含该项目的实例 → 全局兜底指针。
**薄 CLI(三期 4**
- `outgoing` / `projects` 是 live-only:不可用时返回**明确原因**,
而不是空结果(空结果会被 agent 读成"没有引用")。
- 项目解析顺序:`--project``--snapshot` → 从 cwd 向上找 mod 项目根。
- 退出码:0 成功 / 2 用法错误 / 3 不可用。
### 已知限制
- `get_asset_references` 不计算 `xai:joinAction``Replace`/`Remove`)合并后的
有效值,需要调用方自行确认。
- 传输层仍是 loopback HTTP(临时端口 + token)。换命名管道/UDS 的收益是
免防火墙弹窗与更自然的权限模型,代价是客户端要从 `fetch` 改为
`http.request({ socketPath })`;因为转发逻辑已集中,改动面只剩
`liveClient.ts`
- 远程 / WSL / 容器场景下 `process.execPath` 是服务端二进制,MCP 必须跑在
远端;本期未处理。
- CLI 在"无 Node 且无 VS Code"的环境下无法运行(这是逻辑必然:它需要一个
运行时)。有 VS Code 时用 Electron 二进制即可。
---
## 19. 三期测试与验证
```
npx tsc --noEmit 通过
test/*.test.mjs 37 个文件 / 302 个用例,299 通过,3 跳过
```
跳过的 3 个都是同一原因:本沙箱禁止从 Node 进程 spawn shell`EPERM`),
所以"真正拉起 launcher / CLI 子进程"的集成用例会优雅跳过并给出原因。
它们在本机正常运行时会执行。
### Electron-as-Node 实测证据
`tools/probe-electron-node.cjs`VS Code 1.135.0 / Electron 42.8.1Windows):
| 项目 | 结果 |
|---|---|
| `ELECTRON_RUN_AS_NODE=1` 生效 | ✅ `process.versions.electron = 42.8.1` |
| Node 版本 | `24.18.1`(系统 Node 24.16.0 |
| 必需模块 | ✅ `fs`/`fs/promises`/`path`/`os`/`http`/`readline`/`zlib`/`crypto`/`util`/`net`/`child_process` |
| `node:test` | ✅ 可载入 |
| `parseLoadedXml` | ✅ |
| `startLocalServer` + HTTP 请求 | ✅ |
| `zlib` gzip/gunzip 往返 | ✅ |
| **打包后 `dist/agent/mcpServer.js` 完整 stdio 会话** | ✅ initialize / tools/list(10 个) / tools/call |
### 无 Node 运行的证明方式
不是卸载 Node,而是证明整条链路不依赖"PATH 上能解析到 node"
1.`cmd /c` + `ELECTRON_RUN_AS_NODE=1` 直接驱动 `dist/agent/mcpServer.js`
→ 完整 MCP 会话成功;
2. 生成的 launcher 里把 **Node 兜底指向一个不存在的路径**,会话仍然成功
→ 证明走的是 Electron 分支;
3. 断言 launcher 文本:Windows 下 Node 分支只能通过
`if not exist "%RA3_RUNTIME%" goto :ra3_node` 到达。
### 其他已验证行为
- **CLI 会自然退出**:实测一次 live 查询后进程 59ms 内自然退出,
`fetch` 的 keep-alive 不会吊住 CLI(否则对 agent 是致命的可用性问题)。
- **live 端到端**`test/agentLiveE2E.test.mjs` 启动真实 `startLocalServer`
按扩展的方式写 instance/endpoint/manifest,然后用**同一个 `LiveClient`**
跑通全部工具,包括 `get_asset_references``definedIn` 继承溯源。
- **跨项目隔离**:为第三个项目创建的客户端在只有 A/B 实例时返回 null,
不会回落到 A 的数据。
- **发现降级**:删掉 `endpoints/` 后仍能通过 `instances/` 找到实例。
- **崩溃剪枝**:插入一个 PID 必死的实例,剪枝后另一个实例仍正常工作;
清理本窗口实例不影响其他窗口。