Files
HomeAgent/plan.md

499 lines
28 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

# HomeAgent 遗留问题清单
> **本文定位(2026-09-25 重写)**:只写**仍未完成、且经源码/实测核实确属本仓库**的问题。
>
> **判定方法**:不继承任何旧档的复选框,回到源码与构建现场逐条重验
> (`grep` 定符号、`go build`、`go test -count=1`、交叉编译试验、双构建试验)。
> 旧档标记「未做」但代码已落地的项、以及引用别仓代码的项,统一进 §三/§五 并附证据。
>
> **本文不写**:已完成项的定位过程(那是 git log 与 commit message 的职责)、
> 无实测支撑的性能断言、任何凭据/生产路径。
---
## 〇、本次重写相对上一版(b6027f3)的三个更正
上一版是源码核实版,结论基本可靠;但本轮**现场实测**推翻了其中三处,记在此处防复发:
1. **「`go test ./...` 有 3 个 FAIL」是环境相关的,不是稳定复现** ——
本机 12:50 实测 `go test -count=1 ./...` **57 包:38 ok + 19 无测试 + 0 FAIL**;
但 11:45 同一命令确实报过 PTY 三例失败(当时 `/dev/ptmx` 报
`permission denied`)。环境恢复后不再复现。
★ **真正的缺陷不是「有无 FAIL」,而是那个 skip 是坏的**:PTY 用例**有**
`t.Skipf("PTY not available: ...")`(`integration_test.go:284/337/395`),
但它查的是 `resp["status"] == "error"`,而 `terminal_create` 失败返回的是
`{"error": "创建终端失败: ..."}`(`agentcli/plugin.go:496`)——**键名不匹配**,
所以环境退化时走的是 `t.Fatalf` 而非 skip。
⇒ 「探测形同虚设」比「没有探测」隐蔽,这才是该修的点(见 §六)。
2. **P2-8「Windows 真机验证」整条作废** —— Windows 支持**已被设计性放弃**
(`cmd/homed/platform_windows.go`:原生 Windows 拒绝启动,改走 WSL2)。
拿「Windows 下 homed 加载插件往返」当验收目标,等于要求一个明确不存在的产物。
3. **新增最高优先级项:C 化 WIP 的三处构建回归** —— 已 **修复**(2026-09-25,见 §二 P0-1)。
原始症状:`linux/arm64` 发布目标必然失败、clean clone 编不出 `homed`。
★ 修复过程中又发现一个更隐蔽的陷阱:**Go 构建缓存不跟踪包外 `#include` 的 C 文件**
(改 C 源码但缓存命中时会静默沿用旧代码)——这对「逐步推进 C 化」是致命的,
已改用包内符号链接避坑。
---
## 一、结论速览
| 类别 | 数量 | 去向 |
|---|---|---|
| 真正待办(本仓库、代码层可动) | 7 项 | §二 |
| 已修复(本轮) | 1 项 | §二 P0-1 |
| 生产部署后验证(代码已就绪,需现场跑) | 4 项 | §四 |
| 已关闭 / 已实现(旧档误标为 TODO) | 8 项 | §三 |
| 跨项目工单(不属本仓) | 2 项 | §五 |
| 明确「不做」(防反复挂账) | 2 项 | §六 |
当前健康度(实测):
- `go build ./...`(宿主)通过;`go test -count=1 ./...` **57 个包:38 ok + 19 无测试文件 + 0 FAIL**
- `CGO_ENABLED=0 go build ./cmd/homed` **必然失败**(`gojieba` 是 cgo-only)——
这是**既有事实**,非本轮问题;`homed` 历来只有 cgo 构建。
- 编解码层双路径(cgo / !cgo)均绿:`make check-codec-paths`。
- `linux/amd64` 与 `linux/arm64` 发布目标均可构建:
`bash deploy/packaging/build.sh linux/arm64 homed` → ELF aarch64(已修,见 P0-1)。
---
## 二、真正待办
按「影响面 × 可验证性」排序。每条给出**证据**(file:line / 实测命令)与**验收**。
### ✅ P0-1 C 化第一刀的构建回归 —— **已修复(2026-09-25)**
**原始问题**(并行 agent 的 `feature/c-core` 工作区改动引入):
1. clean clone 编不出 homed:cgo `LDFLAGS` 硬指向 `csrc/build/libha_codec.a`,
而该 `.a` 既不入库(`.gitignore` 的 `build/` 命中)又不被发布脚本生成
2. `linux/arm64` 发布目标必然失败:宿主 x86-64 的 `.a` 被链进 aarch64 产物,
报 `file in wrong format`
3. `Makefile` arm64 目标缺 CC/CXX,且无 `.syso` 隔离
**修复方式**(判据:不是「能跑」,而是「不能被静默绕过」):
| 问题 | 修法 |
|---|---|
| 链接预构建 `.a` | 改为**包内符号链接** `internal/agent/api/ha_codec.{c,h}` → `csrc/` 权威源;cgo 直接编源码 |
| 交叉编译架构错 | 编源码按目标重编,天然正确 |
| arm64 缺工具链 | `Makefile` 补 `CC_ARM64`/`CXX_ARM64` 与 `.syso` 隔离(照抄 `build.sh` 已有做法)|
| 双构建无覆盖 | 新增 `make check-codec-paths`(cgo 与 !cgo 两条都跑)|
★ **为何不用 `#include "../../../csrc/src/ha_codec.c"`(包外相对包含)**:
**Go 构建缓存不跟踪包外被 `#include` 的 C 文件**。实测:在包外源里把返回值从 7
改成 8,`go test` 依然通过(缓存命中、静默沿用旧代码);同样改动落在包内文件时
立即判红。对「逐步推进 C 化」这是致命的——改 C 源码却不生效且无任何报错。
(包内 shim `#include` 包外源同样漏跟踪,已实测排除。)
**实测证据**(每条都可复现):
- `make build-linux-arm64` → **ELF 64-bit LSB executable, ARM aarch64**(82MB)
- `bash deploy/packaging/build.sh linux/arm64 homed` → **ELF aarch64**(79MB)
- 移走 `csrc/build/` 后,homed(cgo)与 waiter(CGO=0)**均能构建**
- 变异 C 源(`result = 131072` → `777`)后**同一缓存**下 `go test` **立即 FAIL**
(改前:仍报 ok = 漏跟踪)
- `make check-codec-paths` 两条路径 OK;`make csrc-test` C 契约测试 100% 通过
- 全量 `go test -count=1 ./...` → **57 包:38 ok + 19 无测试 + 0 FAIL**
**遗留(不阻塞,已记入 `docs/zh/c-core/llm-orchestration-c.md` §七)**:
- 下一个切片选谁(L1 剩下的协议编解码,依赖 JSON 解析 ⇒ 先定 `ha_json.c` 复用还是新写)
- 符号链接是本仓**首例**(先例数=0)。已验证 git 往返保留(mode 120000),
且 `core.symlinks=false` 降级时会**响亮报编译错**(非静默错误);扩张前应有意识
---
### P0-2 §11.6 reload 语义仍在说谎
**现状**:`plugin_install` 依旧回 `reload_required`,而**不可热重载的插件
(Go `plugin.Open` 路径、`DF_1_NODELETE`)会被假装重载成功**。
**证据**:
- `internal/plugins/pluginmgr/plugin.go:528 / :548 / :755` —— 仍返回 `reload_required`
- 全仓库 `grep -rn "DF_1_NODELETE\|NODELETE" --include=*.go` **为空**(无检测)
- `internal/plugin/registry.go` 注释已自认:`Go plugin.Open 路径(dynamicPlugin)不可卸载,跳过`
**子任务**:
1. ELF 检测 `DF_1_NODELETE` → 标记插件「不可热重载」(`internal/plugin/dynamic_loader_unix.go`)
2. `ReloadOne` 对这类插件返回「需重启 homed」,停止假装成功
3. `plugin_install` 返回 `restart_required` 替代误导性的 `reload_required`
**验收**:
- [ ] 装一个 `DF_1_NODELETE` 插件后,调用方拿到 `restart_required`,不是 `reload_required`
- [ ] `ReloadOne` 在该插件上返回明确错误,且**摘除旧注册面**(工具/stage/output)仍完成,
不留悬空闭包
---
### P0-3 §13.7 RuntimeManager + 分组 worker(唯一架构大件)
**现状**:`RuntimeManager` / `worker_group` / `WorkerGroup` / `RuntimeClient`
在全仓库**零引用** —— 该能力从未实现。当前是**一插件一子进程**
(`internal/plugin/proc/plugin.go:134` 的 `Spawn`)。
**为什么要做**:N 个外部插件 = N 个常驻进程 + N 份 transport,进程数随插件线性涨。
目标是一个 RuntimeManager + 少量 worker + 多插件共享 transport + 每插件独立
PluginContext(独立身份,共享管道)。
**证据**:
- `internal/plugin/proc/plugin.go:134`(`Spawn`,逐插件起进程)
- manifest 无 `worker_group` 键(`third_party/homeagent-sdk/example/a2a/plugin.json`
的键集合为 `author/description/entry/name/name_en/name_zh/platforms/tags/version`)
**子任务**:
1. `RuntimeManager` 类型:worker 池 + 调度
2. `Worker` 类型:一个进程承载多个插件,共享 `RuntimeClient`
3. `PluginContext` 类型:每插件独立身份(能力门、ownerID、工具命名空间)挂在同一 transport
4. manifest 增 `worker_group`(缺省 = 全部归同一 worker,兼容迁移)
5. 高风险插件可声明独立 `worker_group`
**验收**:
- [ ] 缺省分组行为与现状逐字节等价(现有 proc 测试全绿,不改协议)
- [ ] 两个插件同 worker 时,各自 `ReclaimOwner` 只回收自己的 arena 块
- [ ] 一插件崩溃不带走同 worker 的另一插件(或明确:带走,并写进文档)
- [ ] manifest 无 `worker_group` 的旧插件可原样加载(向后兼容)
> 注:旧档称 `.pi/subagents/missions/` 里有一份针对本项的只读架构评审可作设计输入——
> **经核实该评审实际失败了**(`0475d2e9`:`EADDRINUSE 127.0.0.1:14010` 进程崩溃,
> `ok:false`、`output:""`,`exitCode:1`)。**没有可用结论**,本项要从零开始设计。
---
### P1-4 §13.2 arena 容量固定,无 Grow/Shrink
**现状**:内核独占的变长分配器已落地(first-fit + 邻块合并 + owner 校验),
但容量**固定 4MB**,用尽即调用失败,不退回内联。
**证据**:
- `internal/plugin/proc/arena.go:99`:`const arenaDefaultCapacity = 4 * 1024 * 1024`
- `arena.go:263 / :283`:超限直接报错
**为什么当时不做**:跨进程 `remap` 会让正在读的对端 SIGSEGV。所以这不是
「补个函数」,而是要先解决**对端可见的地址稳定性**。
**可选路径**(择一,需先定方案再动手):
- a) 预映射大虚拟区间(未触碰页不占物理内存),容量「逻辑无限」
- b) 段表 + 多段拼接:新块分配在新段,不必 remap 旧段
- c) 维持固定容量,但**把超限错误做成可执行指引**(告诉插件怎么分片)
**验收**(按所选路径定):
- [ ] 超过当前 4MB 的单次 payload 仍能送达,或收到带指引的明确错误
- [ ] 任何情况下对端不 SIGSEGV
---
### P1-5 §12.3 事件环:机制完成,零真实负载检验
**现状**:事件环(区内 segment + eventfd)与 `events.subscribe` 能力已完成,
但**没有一个真实外部插件订阅 `stage` / `tool_call` 事件**,`dropped` 在长跑下的
行为无人观察。
**证据**:
- `internal/plugin/proc/capability.go:149-150`(subscribe/unsubscribe 已授权)
- `internal/plugin/proc/corehandler.go:46-58`(`EvtRingSubscriber` 接口)
- `third_party/homeagent-sdk/example/` 下 21 个 example 中无事件订阅样例
**子任务**:
1. 写一个订阅 `stage` / `tool_call` 事件的 example 插件,跑真实负载
2. 长跑观察 `dropped` 是否异常增长(环 cap 溢出告警是否够用)
**验收**:
- [ ] example 插件能稳定收到事件并正确反序列化
- [ ] 压测下 `dropped` 有上界且可观测
---
### P1-6 §11.2 工具超时措辞误导 + browser 插件超时聚集
**现状**:内核侧工具超时文案仍写「已取消」,但**进程内 cgo 插件根本取消不了**
(OS 线程永久占用)——措辞与事实不符。
**证据**:
- `internal/agent/core/toolcall.go:42`:
`fmt.Sprintf("工具 %s 执行超时(60秒),已取消", tc.Name)`
- `internal/plugin/proc/process.go` 注释自认 cgo 路径「超时后 OS 线程永久占用」
**子任务**:
1. 措辞改「已放弃等待(插件仍在后台运行,其占用的线程无法回收)」
2. 排查 `browser` 插件为何频繁 60s 超时(旧档记录 22/26 次集中于此);
真取消能力依赖子进程模型(与 P0-3 相关)
**验收**:
- [ ] 超时文案不再暗示「已取消」
- [ ] browser 超时率下降到可解释水平,或给出根因
> 与 P0-3 的关系:把内部 cgo 插件也迁到子进程后,这条的严重性自动消除。
---
### P1-7 WebUI `handleAgentAction` 直接 501
**现状**:WebUI 的 agent 操作接口**所有 action 一律返回 `501 Not Implemented`**,
是明确的未接线桩。
**证据**:
- `internal/plugins/webui/handler_agents.go:239`:
`writeJSON(w, http.StatusNotImplemented, ..."action not implemented by supervisor")`
- 路由已注册(`handler.go:404` 的 `/api/v1/agents/`)
- **但 UI 未暴露入口**:`dashboard.js` / `cmd/gui/renderer/app.js` 均无对该端点的调用
**子任务**:决定该端点该做什么——接线到 supervisor 的真实动作
(stop/restart/snapshot),或**直接删掉路由**(UI 既然没用,留着只是待爆的债)。
**验收**:
- [ ] `/api/v1/agents/<id>/<action>` 要么真的能动作,要么不再存在(无 501 桩)
---
### P2-8 §12.4 Lua 仍走独立 ABI(进程内解释器)
**现状**:Lua 插件在**内核进程内**跑 gopher-lua,不走 proc 通道,是三套 ABI 里
唯一没收敛的。代价:Lua 插件崩溃 = 内核崩溃,且无共享内存数据面。
**证据**:
- `internal/plugin/lua_plugin.go:947`(`makeStageHandler`,进程内)
- `internal/plugin/lua_plugin.go:334 / :1270`(`register_stage` 直接注册到内核 SDK)
**子任务**:评估「Lua 走 proc 通道」的代价(解释器进程启动开销 vs 隔离收益),
据此决定收敛还是明确保留为独立 ABI 并写进文档。
**验收**:
- [ ] 有明确决策(收敛 / 永久保留),且文档与实际一致
---
## 三、已关闭 / 已实现(旧档误标或本轮更正,防复活)
逐条给出「旧档怎么说」与「实际怎样」。
### 1. §13.12 L3 原生多模态 —— 已实现
- 实际:`internal/memory/graph.go:169` 起建 `memory_blocks`
(含 `modality/payload_digest/mime/vector/fingerprint/scene`)+
`memory_block_edges`(`source_kind/target_kind/edge_type`),以及
`scenes`/`scene_features`/`scene_refs`;`internal/agent/core/graphmedia.go`(310 行)
实现 `migrateLegacyGraphMedia`/`attachBlocksToSentence`/`linkBlocksToDocument`/
`commitTriplesWithMedia`;`graphmedia_test.go` 21 个测试。
- 结论:**关闭**
### 2. §13.9 llmsproxy 上下文溢出感知 —— 不属本仓(见 §五.1)
### 3. §13.10 AgentMail 三个 bug —— 不属本仓(见 §五.2)
### 4. §11.4 Lua stage 快照缺读锁(DATA RACE)—— 已修
- 实际:`internal/plugin/proc/shmcodec.go:42` 的 `WriteAll` 已在 `captureLocal`
前后持 `sc.RLock()/RUnlock()`;`go test -race ./internal/lua/... ./internal/plugin/...` 全绿
- 结论:**关闭**
### 5. §12.2 `io.setToolBlocks` 内核侧是桩 —— 已实现
- 实际:`internal/plugin/proc/corehandler_inject.go:132` 实现
`MethodIOSetToolBlocks`;模板 `putArena` → `blocks_ref`;
`e2e_template_test.go:371` 用**真实 SDK 模板**验证
- 结论:**关闭**
### 6. §13.11 WebUI 修复清单 —— 主体已实现,仅剩 P1-7
- 逐项核实:`Last-Event-ID` 重放(`handler_chat.go:710`)、请求超时
(`handler_chat.go:592` 等 300s)、XSS 消毒(`dashboard.js:252` DOMPurify)、
`renderAll` 增量(`dashboard.js:34 / :452 / :1361` 增量游标 + 流式增量)、
`handleKnowledge` 不再吞错(`handler_memory.go:122` 起逐分支返回错误)、
CSS/DesignSystem(`dashboard.css:3` 起 sakura/frost 令牌)、
GUI 重构(`cmd/gui/renderer/app.js`)—— 均已落地。
- 仅 `handleAgentAction` 501 是真缺口(已列 P1-7)
### 7. Windows 支持 —— **已设计性放弃**(旧档 P2-8 与 C 化 §2.3 的前提均据此更正)
- 旧档说:「Windows 桩已收敛,但缺真机验证」(把它当待办)
- 实际:`cmd/homed/platform_windows.go` 明确**原生 Windows 拒绝启动**并给 WSL2 指引。
原因写入注释:插件体系依赖「继承的 fd」+「统一共享内存区的段内偏移解引用」,
Windows 句柄模型无法表达;强适等于再维护一套平台专属 ABI(C ABI 时代三套 ABI
并存曾致改写型插件静默失效)。
- 配套:`internal/plugin/proc/shmalloc_windows.go` 的 `allocShm` 直接报错不返回半可用段;
`deploy/packaging/windows/install-via-wsl.ps1`(新)引导 WSL2 并复用 Linux 包;
`build.sh` windows 目标**只构建 waiter + gui**,homed/initconfig 明确拒绝
(见 `build.sh:257-267`)。
- 实测佐证:`GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build ./cmd/waiter` 成功
(12MB .exe);`homed` 无论如何都编不出 Windows(`internal/memory` 依赖 cgo-only 的
`gojieba`)。
- 结论:**关闭**(该项不是待办;Windows 的正确验收 = WSL2 内按 Linux 路径跑,
与 Linux 目标同一条流水线)
### 8. 仓库卫生:PTY 三例的 FAIL —— 环境相关 + skip 判据失效(旧档 P2-10)
- 旧档说:`go test ./...` 有 3 个 FAIL(PTY 三例 / 端口 9890 冲突 / `system_test` 写 `/etc`)
- 本轮实测(分时)时:
- `go test -count=1 ./...` → **57 包:38 ok + 19 无测试文件 + 0 FAIL**
- PTY 三例**本就有 skip 意图**(`integration_test.go:284/337/395` 的
`t.Skipf("PTY not available: ...")`),且 `/dev/ptmx` 可用时正常通过(连跑 3 次均 ok)
- **但该 skip 的判据是坏的**:它查 `resp["status"] == "error"`,而插件失败时返回的是
`{"error": "创建终端失败: ..."}`(`internal/plugins/agentcli/plugin.go:496`)——
**键名不匹配**,于是真遇到无 PTY 权限的环境会走到 `t.Fatalf` 而非 skip。
这是「探测存在但失效」的典型:比没有探测更隐蔽
- `TestRestoreFileFromBaseline` 在 `/etc` 可写时 **PASS**(`system_test.go:83` 确实
写真实路径且不检查 err——**代码确实不干净**,但它不构成「稳定 FAIL」)
- `9890` 端口**确有占用**(本机 homed 常驻监听),但测试用 `setupIntegration`
起的实例未与之冲突(连跑 3 次均 ok)
- 结论:**旧档的记录在当时是真的**(环境退化:ptmx 无权限 + 端口被占),
环境恢复后自然全绿。但**两个真缺陷存留**:① skip 判据键名不匹配(探测失效,
退化时硬 FAIL);② 测试依赖固定端口。两条已列 §六,不列为「待办功能项」。
---
## 四、生产部署后验证(代码已就绪,本就无法在仓库内完成)
这些**不是待开发项**,是「必须落到生产实例才能确认」的验收。仓库内有
`scripts/verify_deploy.sh [data_dir]` 可一键检查前两条。
- [ ] **§0.1 healthcheck 隔离**:部署后 `knowledge/`、`memory/graph.db`、
`memory/documents/` 不再出现 `_hc_*` 残留
- [ ] **图记忆去重**:`relations` 重复率归零,跑一周不新增重复
- [ ] **§13.5 / §13.6 QQ 端到端**:真实 QQ 消息注入与输出经共享内存通道正常
(小 payload 内联、大 payload 走 `text_ref`/`frame`)
- [ ] **§0.2 agentcli 不泛滥**:QQ 消息在 agentcli 无自喂送风暴时能被正常响应
---
## 五、跨项目工单(**不属本仓**,旧档误并入)
旧 plan.md 把别仓的工单写成本仓 TODO,导致「查无此代码却挂着未完成」。移出并说明归属:
### 1. §13.9「llmsproxy 上下文溢出感知」
- 旧档写:补 `OVERFLOW_PATTERNS`("Context window is full")、AUTO 截断宽度 `80→160`、
`go test ./internal/ai/...`
- 事实:本仓**没有 `internal/ai/`**;相关符号在 **`/home/program/llmsproxy`**
(`internal/gateway/chat.go`)。本仓 `internal/agent/api/provider.go` 只**消费**
该网关(注释里提到 "llmsproxy 的 AUTO 链",:361)
- 现状:该仓已把宽度改成 160
- 归属:**llmsproxy 仓**
### 2. §13.10「AgentMail 三个 bug」
- 旧档写:提示词修正 / `InReplyTo` / `relay_key ≤ 64 字节`
- 事实:AgentMail 是独立仓 **`/home/program/agentmail`**
(`relay_key` 见 `server/internal/handler/permission.go`)。本仓
`grep relay_key\|InReplyTo` **零命中**
- 归属:**agentmail 仓**
> 若这两仓也要纳入统一管理,应各自建 plan,不要塞进本仓文档。
---
## 六、明确「不做」与「建议修但不阻塞」
### 不做(防反复挂账)
- **§13.13 反向结果入共享内存**:本仓**不存在 `llm.chat`**(`llm.*` 只映射
listSources/setSource/currentSource);唯一可能返回大结果的 `doc.query` 被
`CapDocMemory` 能力门挡着,且无外部插件使用。**不做**。
- **Windows 原生适配**:见 §三.7,**设计上不做**。
### 建议修但不阻塞(测试卫生,非当前 FAIL)
- `internal/system/system_test.go:83`:`target := "/etc/RestoreFileFromBaseline.test.tmp"`
写真实系统路径,且两处 `os.WriteFile(...)` **不检查 err** → 在 `/etc` 不可写的
环境里静默失败,报 `expected restore to happen`(根因是测试,不是实现)。
建议改用 `t.TempDir()` + 保留 `IsProtectedPath` 语义所需的显式前缀,并检查每步 err。
- ✅ **(已修,2026-09-25,`17e7094`)固定端口冲突**:webui `:8080`、
`pluginmgr` `127.0.0.1:9876`(曾是包级可变全局 `var HTTPAddr`,多实例互相踩)、
`remotedevice` `127.0.0.1:9890`。
修法:pluginmgr 改为实例字段;remotedevice 改显式 `net.Listen`(失败同步可见、
`:0` 能回报真实端口);测试经新增的 `ConfigRegistry.SetPluginConfig` 在插件
加载前预置 `127.0.0.1:0`,三个插件各自绑 OS 分配的空闲端口。
验证:`internal/plugins` 连跑 **30/30**(修前干净树 18/20)。
- ✅ **(已修,2026-09-25,`17e7094`)`TestRealPlugin_DeepSearchInvoke` 把上游限流当回归**:
插件在上游限流时返回的是**正常结果**(err==nil,content 含「未返回结果」+ 无响应
引擎列表),那是外部条件。现区分「上游不可用(限流/CAPTCHA)⇒ t.Skip 带理由」
与「其他异常 ⇒ fail」,不再让噪声淹没真回归。
- **PTY 三例的 skip 判据是坏的(真缺陷,不只是卫生)**:`integration_test.go`
284/337/395 查 `resp["status"] == "error"`,而 `terminal_create` 失败时返回
`{"error": "创建终端失败: ..."}`(`internal/plugins/agentcli/plugin.go:496`)——
键名不匹配 ⇒ 真遇到无 PTY 权限的环境**会 `t.Fatalf` 而非 skip**。
同类型:其他依赖工具错误响应的环境探测(应统一认 `error` 键)。
- 同类审计:其他「写真实系统路径且吞错误」的测试。
---
## 七、C 化:已定事项与待拍板事项
第一刀(L1 纯函数层)已落地并闭环(见 §二 P0-1 与
`docs/zh/c-core/llm-orchestration-c.md`)。下阶段扩大前,有三处**需 jianf 拍板**:
1. **C 实现放主仓 `csrc/` 还是 SDK `third_party/homeagent-sdk/`?**
- 当前已在主仓 `csrc/`(`ha_codec.{c,h}` 是权威源,Go 侧符号链接过去)
- 若 SDK / 鸿蒙 / C SDK 侧也要复用,需定同步机制;搬进 SDK 则要走 SDK 冻结流程
2. **`ha_json.c` 复用还是新写?**(复用会动 SDK 目录结构)
- 这是下一个切片的**前置**:L1 剩下的协议编解码(`parseOpenAICompatible*`、
`normalize*ToolCalls`)全部依赖 JSON 解析,不定就推不下去
3. **下一个切片选谁?**(已有基准数据支撑,见下)
### ★ 已定:编解码层「完全 C 化」(2026-09-25,jianf 裁定)
初版基准一度得出「C 比 Go 慢」,**该结论已被推翻** —— 根因是我的 Go 绑定与 C 实现
写得烂(每次调用 `CString` malloc+拷贝 + C 侧 `strlen` + `lower_dup` malloc +
逐字节扫描),把自找的 82% 开销误当成了 cgo 的固有成本。拆解实测:
| 场景 | ns/op |
|---|---:|
| cgo 边界(零拷贝 + 空函数体)| **31.9** ← cgo 真实固有成本 |
| + `C.CString` + `strlen` | 105–111(多出 ~75ns)|
| 初版 `ModelContextWindow` | 175 |
优化后(零拷贝传指针+长度、栈缓冲折叠、字级 ASCII 检测、位运算 UTF-8 校验、
截断返回字节数、提前短路;纯 Go 侧也去掉 `[]rune` 分配):
| 基准 | 初版 C | 优化后 C | 纯 Go | 提升 |
|---|---:|---:|---:|---:|
| `ModelContextWindow` | 175 | **76.5** | 46.8 | 2.3× |
| `EstimateTokens` / 1KB ASCII | 2318 | **80.8** | 326 | **28.7×** |
| `TruncateByTokens` / 1KB ASCII | 2594 | **71.7** | 411 | **36×** |
| `TruncateByTokens` / 1KB 中文 | 3923 | **70.1** | 3097 | **56×** |
**裁定:完全 C 化,不做按长度分派。** 我一度加了「短串走回 Go」的分派,
被否决 —— 那会同时存在两份语义可能分叉的实现。代价如实记录:
`EstimateTokens("qq")` 这类极短串上 C 约 47ns vs Go 约 3ns(几乎全是边界成本),
绝对值纳秒级可忽略;若某循环对极短串高频调用,**正确应对是 C 化那个循环
(批量传一次)**,而不是退回 Go(见下条)。
**结论性变化**:
- C 侧新增**与 Go `utf8.DecodeRuneInString` 等价的完整校验**(含过长编码、
代理对、超 U+10FFFF、截断序列)。这使中文密集输入比初版慢(1467 vs 840),
但初版对畸形序列会与 Go **分叉**(rune 计数偏差 ⇒ token 预算/截断点偏移,
只影响计数不会崩,不测发现不了)。正确性优先,且仍比纯 Go 快 2×。
由 `TestGolden_InvalidUTF8`(3000 组随机字节)钉死。
- 包**要求 cgo 才能编译**(删除了 `!cgo` 回退):CGO_ENABLED=0 下整包构建失败。
理由:不许存在第二条可能分叉的实现路径;且 `waiter`/`initconfig`/`memgc`/`mock-server`
实测均**不依赖**本包,无 CI 在 CGO_ENABLED=0 下构建它 ⇒ 不影响任何现有构建。
Makefile 的 `check-codec-cgo-only` 把「不许有第二条路」变成可执行断言。
**下一个切片的判据**(已从「哪个看起来底层」换成「哪个在真实分布下真能变快」):
协议编解码(`parseOpenAICompatible*` / `normalize*ToolCalls` / SSE 分片)
处理的正是**长文本**,是 C 的主场,比「把短函数搬过去」更合理。
但其前置是 JSON 解析 ⇒ 先定 `ha_json.c` 复用还是新写。
### 已定事项(不再挂账)
- **不保留回退路径**:编解码层**要求 cgo 才能编译**(无 `!cgo` 文件)。
CGO_ENABLED=0 下整包构建失败——这是有意的响亮失败,不是缺漏。
**为什么不做回退**:回退会让两份语义可能分叉的实现同时在产线跑。
C 侧对畸形 UTF-8 的解码边界一旦与 Go 分叉,只表现为 rune 计数偏差
(⇒ token 预算与截断点偏移),**不会崩、不会报错**,最难发现。
只验证过一条路,就不该存在第二条。
**为什么这不影响任何构建**(实测,别当成风险):
- `go list -deps` 实测**只有 `cmd/homed` 依赖 `internal/agent/api`**;
`waiter` / `initconfig` / `memgc` / `mock-server` 均**不依赖**(逐个验过)。
- homed 本就强制 cgo(mattn/go-sqlite3 + gojieba)⇒ 恒走 C 路径。
- 仓库**无 CI**(无 .github/workflows),发布脚本仅在构建 `waiter` 时用
CGO_ENABLED=0,而 waiter 不依赖本包。
- `make check-codec-cgo-only` 把「不许有第二条路」变成可执行断言
(断言 cgo 下全绿 **且** CGO_ENABLED=0 下必须失败)。
- **纯 Go 实现的定位**:`codec_pure.go` 不带 build tag、永远编译,
但**不是生产路径**——它只作**黄金对照的规格基准**与可读规格。
(它本身也已零分配化:去掉 `[]rune` 的 4×len 临时分配。)
- **不链接预构建 `.a`,也不用包外 `#include`**(前者架构错 + 产物两头空,
后者缓存漏跟踪)。用包内符号链接,理由与实测见 §二 P0-1 与设计文档 §2.4。
---
*重写时间:2026-09-25 · 核实方式:源码 grep / go build / go test -count=1 /
交叉编译对照试验 / 双构建试验 / 变异测试*
*旧档备份:`/tmp/plan.md.bak-20260925-124700`(上一版)、
`/tmp/plan.md.bak-20260924-171420`(历史 1474 行混合档)*