C 化第二刀:为协议编解码层铺 JSON 底座。**本刀只交付库 + 验收,
未改 Go 生产路径**(接线是独立一步,库先验完再换产线)。
为什么是它:SSE 单块解析(parseOpenAICompatibleStreamChunkFull)是每个流式
chunk 都要跑的最热路径,实测 1937ns/13allocs(content 块)、3122ns/21allocs
(toolcall 块),而纯字节扫描理论下限 133ns/1alloc —— 差距 15~23×。
一次 1 万块的会话 = 1~2 万次堆分配,正是 GC 抖动的来源。
为什么不复用 SDK 的 remotedevice/ha_json.c(实测三缺陷,不可直接复用):
① 无 \u 解码:\u4f60\u597d → ?0?d?d?0(非 ASCII 全靠转义时内容直接损坏)
② 只有 _get_int 无浮点:temperature:0.7 静默变 0
③ null 与「键缺失」不可区分
外加它是 DOM + malloc,与本层「不 malloc / 零拷贝 / 纯函数」正交。
设计:scan(结构,零分配零解码)+ extract(取值,按需解码)两段分离。
content 可能是很大的多模态数组,而 stringifyContent 只需要 text 字段拼起来;
若 scan 就解码并分配缓冲,等于把成本付给不需要它的调用方。
★ 被测试抓出 7 个真实缺陷(写 C 时同一逻辑我读三遍都认为正确):
1 代理对合成成功后未跳过 unconditionally 的 U+FFFD 发射(😀 → 两个 FFFD)
2 过长编码检查用了只含首字节位的 cp(「你」→ 6 个 FFFD)
3 members_next 只报值起点不消费值 → 游标停在值前(模糊测试第一轮抓到)
4 扫描阶段不校验转义字符合法性({"a":"\q"} C 判合法、json.Valid=false)
5 扫描阶段不校验 \u 后四位十六进制(同上)
6 get_int 接受前导零(007 / 00)
7 cgo 桥接把 C 结构体声明为 Go 局部变量 → 运行时 panic
(cgo argument has Go pointer to unpinned Go pointer)
其中 4 个是「静默分叉」——不崩、不报错,生产里表现为「内容少一个字符」
或「某些块被静默丢弃」,极难归因。这正是黄金对照不可省的理由。
★ 另纠正我自己两次错误的「真值」(比代码 bug 更危险,会变成错误规格):
第一版真值表里 content:{} 的花括号少了一层,把「我写错 JSON」误读成
「Go 对 content 严格」。修正后实测发现一对方向相反的语义:
content 走 interface{} 宽松({}→"{}"、true→"true"),
reasoning_content/usage/finish_reason 强类型严格(123 ⇒ 整块作废)。
照错误表写 C 会产出「比 Go 更严格」的实现,静默丢弃本该生效的块。
两个由缺陷倒逼的设计决定:
- members_next 返回**完整值 span** 并内部跳过 ⇒ 「返回 1」蕴含「成员良构」。
要求调用方自己推进游标的 API 是错的:忘一次就解析到上一个值且不报错。
- members_complete() 区分「正常扫到 }」与「输入畸形」,否则无法复刻 Go 严格性。
同时修两个基础设施目标对「多源文件/多测试」的适配:
- csrc-sanitize:每个契约测试各自链接(多个 main 合链会 multiple definition,
而报错被吞后会被误报成「本机无 sanitizer」——一个假的 SKIP)
- csrc-cross:多源文件改用 -fsyntax-only 逐文件(gcc 不支持多源单 -o)
实测(全部当场可复现):
- C 契约测试 119 项断言全过;黄金对照 5 组全过(语法/成员/解码/整数/随机字节)
- libFuzzer 4948 万次运行零崩溃(121s)
- ASan+UBSan PASS(两个契约测试各跑);gcc+clang 零告警;arm64 交叉编译 0 告警
- 全量 go test -count=1 ./... 0 FAIL;make build-linux-arm64 → ELF aarch64
- 纪律检查 SDK 公开接口 diff = 0 行(未触碰 SDK)
决策关闭(jianf 本轮裁决):C 实现留主仓 csrc/(它本就是替换内核 Go 实现,
SDK 从未被触碰,跨端复用才需进 SDK 而它们不调用本层);ha_json.c 不复用;
下一刀即协议编解码层。
34 KiB
HomeAgent 遗留问题清单
本文定位(2026-09-25 重写):只写仍未完成、且经源码/实测核实确属本仓库的问题。
判定方法:不继承任何旧档的复选框,回到源码与构建现场逐条重验 (
grep定符号、go build、go test -count=1、交叉编译试验、双构建试验)。 旧档标记「未做」但代码已落地的项、以及引用别仓代码的项,统一进 §三/§五 并附证据。本文不写:已完成项的定位过程(那是 git log 与 commit message 的职责)、 无实测支撑的性能断言、任何凭据/生产路径。
〇、本次重写相对上一版(b6027f3)的三个更正
上一版是源码核实版,结论基本可靠;但本轮现场实测推翻了其中三处,记在此处防复发:
- 「
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。 ⇒ 「探测形同虚设」比「没有探测」隐蔽,这才是该修的点(见 §六)。 - P2-8「Windows 真机验证」整条作废 —— Windows 支持已被设计性放弃
(
cmd/homed/platform_windows.go:原生 Windows 拒绝启动,改走 WSL2)。 拿「Windows 下 homed 加载插件往返」当验收目标,等于要求一个明确不存在的产物。 - 新增最高优先级项: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 FAILCGO_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 工作区改动引入):
- clean clone 编不出 homed:cgo
LDFLAGS硬指向csrc/build/libha_codec.a, 而该.a既不入库(.gitignore的build/命中)又不被发布脚本生成 linux/arm64发布目标必然失败:宿主 x86-64 的.a被链进 aarch64 产物, 报file in wrong formatMakefilearm64 目标缺 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-testC 契约测试 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)不可卸载,跳过
子任务:
- ELF 检测
DF_1_NODELETE→ 标记插件「不可热重载」(internal/plugin/dynamic_loader_unix.go) ReloadOne对这类插件返回「需重启 homed」,停止假装成功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)
子任务:
RuntimeManager类型:worker 池 + 调度Worker类型:一个进程承载多个插件,共享RuntimeClientPluginContext类型:每插件独立身份(能力门、ownerID、工具命名空间)挂在同一 transport- manifest 增
worker_group(缺省 = 全部归同一 worker,兼容迁移) - 高风险插件可声明独立
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 * 1024arena.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 中无事件订阅样例
子任务:
- 写一个订阅
stage/tool_call事件的 example 插件,跑真实负载 - 长跑观察
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 线程永久占用」
子任务:
- 措辞改「已放弃等待(插件仍在后台运行,其占用的线程无法回收)」
- 排查
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.go21 个测试。 - 结论:关闭
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:252DOMPurify)、renderAll增量(dashboard.js:34 / :452 / :1361增量游标 + 流式增量)、handleKnowledge不再吞错(handler_memory.go:122起逐分支返回错误)、 CSS/DesignSystem(dashboard.css:3起 sakura/frost 令牌)、 GUI 重构(cmd/gui/renderer/app.js)—— 均已落地。 - 仅
handleAgentAction501 是真缺口(已列 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.shwindows 目标只构建 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、pluginmgr127.0.0.1:9876(曾是包级可变全局var HTTPAddr,多实例互相踩)、remotedevice127.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.go284/337/395 查resp["status"] == "error",而terminal_create失败时返回{"error": "创建终端失败: ..."}(internal/plugins/agentcli/plugin.go:496)—— 键名不匹配 ⇒ 真遇到无 PTY 权限的环境会t.Fatalf而非 skip。 同类型:其他依赖工具错误响应的环境探测(应统一认error键)。 - 同类审计:其他「写真实系统路径且吞错误」的测试。
七、C 化:已定事项与待拍板事项
✅ 已落地:C 基础设施门禁(2026-09-26,8070844)
在推进下一刀之前先把地基建起来 —— 没有门禁,每个 C 切片都在裸奔。
| 门禁 | 内容 | 为什么不能省 |
|---|---|---|
csrc-lint |
gcc+clang × -Wall -Wextra -Wpedantic -Wshadow -Wconversion,零告警才过 |
C 侧没有 Go 的 vet 等价物,告警是唯一静态信号;-Wconversion 专门盯 cgo 窄化(size_t→int 截断默认静默) |
csrc-abi |
ha_abi.h 的 ABI 主/次版本 + 运行期自述 + Go 侧常量,三方交叉断言 |
「签名冻结」原本只写在注释里,注释不参与编译 |
csrc-headers |
每个 .h 能单独编过 |
缺 include 时只在「恰好被别的头先包含」处静默编过 |
csrc-sanitize |
ASan + UBSan 跑契约测试 | C 侧内存错误/UBSan 默认静默(不崩、结果看着对),而零 malloc 设计依赖「无越界写」 |
csrc-cross |
arm64 交叉编译 | go build 不等于 C 代码在该架构上能编 |
csrc-fuzz |
libFuzzer:内存安全 + 6 条不变式 | 畸形 UTF-8 等价性正常输入永远测不到,只有随机字节覆盖得到 |
聚合入口 make check-csrc(已接进 make test)与 make check-csrc-full(含模糊测试)。
地基上线第一小时就抓出 5 个真 bug,全是我自己写的基础设施代码 —— 这本身就是门禁有效的证据:
_Static_assert是 C11,而项目 CFLAGS 是-std=c99(-Wpedantic报的)- C99 分支
##msg拼接字符串字面量 ⇒ 两个断言共用一个 typedef 名(clang 报的) - bench 用 POSIX
clock_gettime,而 CMake 刻意C_EXTENSIONS OFF⇒ 未声明 - 头文件缺 include 的静默通过
- Go 不允许在
_test.go里用 cgo,且 cgo 生成的*_Cvar_*不是 Go 常量
验证(全部当场可复现):libFuzzer 91s / 3,329,316 次运行 / 零崩溃;
变异测试证明门禁不摆设(改 C 宏、或让 Go 常量与 C 函数「一起错成一样」均被判红);
双编译器 × c99/c11 零告警;ASan+UBSan PASS;arm64 交叉编译 0 告警;
全量 go test -count=1 ./... 57 包 0 FAIL;make build-linux-arm64 → ELF aarch64。
首次把两类成本分开测(这是基础设施的核心价值之一): C 侧纯函数基准给出的函数体成本与 Go 侧基准里的 cgo 边界成本(~30ns) 从此可分离 —— 否则看到某场景慢,根本分不清该优化 C 函数体、还是该减少跨语言调用次数。
| 场景 | C 函数体(纯 C,无边界) |
|---|---|
estimate_tokens ascii_1k |
121.7 ns / 8.4 GB/s |
estimate_tokens zh_1k |
1434 ns / 0.71 GB/s |
truncate_by_tokens ascii_1k |
134 ns |
truncate_by_tokens zh_1k |
128 ns |
注:estimate_tokens 的中文与 ASCII 差距 11.8×(与 Go 侧 bench 注释「zh_1k 3097ns」的
量级关系一致)。这是「中文字节校验」的固有成本,不是缺陷;但它是下一个切片的输入 ——
若某热路径以中文为主,优化点在这里,不在 cgo 边界。
★ 下一刀的决定:协议编解码层(实测支撑,不再是「哪个看起来底层」)
为什么是它:SSE 流式分块解析 parseOpenAICompatibleStreamChunkFull 是
每个流式 chunk 都要跑一次的最热路径,而它当前每次付费 12–21 次堆分配:
| 输入(真实负载形状) | ns/op | allocs/op |
|---|---|---|
| content 块(含中文) | 1937 | 13 |
| toolcall 块 | 3122 | 21 |
| usage 块 | 2464 | 12 |
| 纯字节扫描理论下限 | 133 | 1 |
差距 15–23×。会话 1 万块 ⇒ 1–2 万次分配,正是 GC 抖动的来源(C 化的原始动机)。
★ ha_json.c:不可直接复用(原判断「仓库里已有现成实现」已被实测推翻)
SDK 的 remotedevice/src/ha_json.c(368 行)经实测有三个对协议层致命的语义缺陷:
| # | 缺陷 | 实测 |
|---|---|---|
| 1 | 没有 \u 解码 |
{"k":"\u4f60\u597d\ud83d\ude00"} → ?0?d?d?0(期望 你好😀) |
| 2 | 只有 ha_json_get_int,无浮点 |
temperature:0.7 → 0(注意:0.7 的 int_val 被静默置 0,不是解析失败) |
| 3 | null 与「键缺失」不可区分 |
二者都返回 NULL / def |
外加架构冲突:它是 DOM + malloc,与 ha_codec 的「不 malloc / 零拷贝 / 纯函数」约束正交;
在热路径用它等于重建刚测出的「每 chunk 十几次分配」。它的定位是
remotedevice 设备通道的 JSON(ha_json_parse 注释显示 \u 一律写 '?'),
不是 LLM 协议层。
建议方案:csrc/ 新建专用 ha_jsonscan.c(零分配、span 返回、增量) ——
对齐 ha_codec.h 的既有三条设计约束(只吃指针+长度 / 不 malloc / 无状态纯函数)。
不复用 SDK 版的四条理由:
- 复用 = 跨仓改动 → SDK 发版 → 两仓版本对齐(违反「双仓绑定」纪律);
- 把内核 LLM 协议层耦合到 SDK 的 remotedevice 设备通道库(架构错位);
\u/浮点/null 三缺陷必须补,补完已等同于重写;- DOM + malloc 与既定约束冲突。
不建议把内核热路径依赖另一个仓 —— 这是本项的核心理由。
✅ 三项已裁决(2026-09-26,jianf)
- C 实现放哪里 → 留在主仓
csrc/。 用户澄清:C 实现本来就是替换内核的 Go 实现(csrc/是唯一权威源,Go 侧符号链接 + cgo 绑定,codec_pure.go已降级为黄金对照的规格基准)。SDK 从头到尾未被触碰 (git diff main -- third_party/homeagent-sdk/sdk/= 0 行)。 所谓「进 SDK」只对跨端复用(鸿蒙/嵌入式/C SDK 也想调同一份实现)才有意义, 而它们并不调用内核编解码层 ⇒ 该问题关闭,不作为待办。 ha_json.c复用还是新写 → 新写。 见上(三个致命缺陷 + 架构正交)。- 下一刀选谁 → 协议编解码层,立即开工。 SSE 单块解析实测 1.9–3.1µs / 12–21 allocs。
第一刀(L1 纯函数层)已落地并闭环(见 §二 P0-1 与
docs/zh/c-core/llm-orchestration-c.md)。基础设施门禁已建成(见上)。
下阶段扩大前,三处需 jianf 拍板(建议已附,见上):
- C 实现放主仓
csrc/还是 SDKthird_party/homeagent-sdk/?- 当前已在主仓
csrc/(ha_codec.{c,h}是权威源,Go 侧符号链接过去) - 若 SDK / 鸿蒙 / C SDK 侧也要复用,需定同步机制;搬进 SDK 则要走 SDK 冻结流程
- 当前已在主仓
ha_json.c复用还是新写?(复用会动 SDK 目录结构)- 这是下一个切片的前置:L1 剩下的协议编解码(
parseOpenAICompatible*、normalize*ToolCalls)全部依赖 JSON 解析,不定就推不下去
- 这是下一个切片的前置:L1 剩下的协议编解码(
- 下一个切片选谁?(已有基准数据支撑,见下)
★ 已定:编解码层「完全 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 行混合档)