Files
HomeAgent/docs/zh/plugin-migration-plan.md
JianFeeeee 53a148cb54 docs: Part 5 标记核心已完成,进度快照更新事件环
Part 5 通知面内核侧 + 模板侧均已完成,端到端测试通过。
事件订阅从 C ABI 的空实现(case 23/24)变成真正可用。
测试数量更新:proc 38 项 + plugin 16 项(含 -race)。
下一步转 Part 6 逐插件迁移。
2026-09-02 17:07:43 +08:00

32 KiB
Raw Blame History

外部插件多进程化适配计划(修改→审查→验证三步微循环)

分支:update 基线:docs/zh/plugin-interface-matrix.md(合同面 A/B/C+ plan.md §11 + docs/zh/架构迁移评估.md 每部分 = 一个「修改 → 审查 → 验证」三步微循环。所有验证在 update 分支完成,可独立交付、可回退。

循环的铁律(每部分适用):

  • 修改:只动核心侧 + 工具链,third_party/homeagent-sdk/sdk/(合同面 A零 diff
  • 审查:接口冻结检查(git diff 公开 SDK 为空)+ 代码 review + go vet
  • 验证make test + 针对性单测 + 端到端冒烟,产物 .bin 端到端可用。

【M】=修改部分、【R】=审查部分、【V】=验证部分。依赖前置部分完成后才可开始。


目录

  • Part 0 脆弱基线先行(不依赖迁移,现网可直接受益)— 0.1 / 0.2 / 0.3 ⏭️ / 0.4 ⏭️
  • Part 1 加载分派骨架(entry 双通道共存)— 已完成
  • Part 2 子进程通道原型spawn / JSON-RPC / procPlugin 已完成
  • Part 3 plugindev 工具链改造(.bin 产物)— 已完成
  • Part 4 共享内存数据面StageContext 跨进程并发改写)— 已完成(段/编解码/锁仲裁 + RunStage 接线)
  • Part 5 通知面(事件环 + eventfd 核心已完成
  • Part 6 迁移与收尾17 插件逐个 + 删 cabi + 权限显式化)
  • 最终验收清单

进度快照2026-09-02:分支 feature/plugin-proc-migration。 已交付:现网止血 2 项11.1/11.3、entry 双通道分派、共享内存 stage 并发、 子进程控制面NDJSON RPC + 51 method 名平移、plugindev .bin 构建、 registry 接线、事件环§3.6外部插件已可端到端跑在子进程 + 共享内存上 且首次获得事件订阅能力C ABI 下 case 23/24 一直是空实现)。 测试:内核 internal/plugin/proc 38 项 + internal/plugin 16 项(含 -race SDK 仓 plugindev 16 项。 下一步Part 6 逐插件迁移 + 删 internal/plugin/cabi/


Part 0脆弱基线先行阶段 0~1 人日)

依据plan.md §11.1/11.3/11.6。不依赖任何新架构,独立交付,现网直接受益。 目的:在副本模型内部打补丁,止血,为后续迁移争取时间。

0.1 output_send 假成功修复11.1)— 已完成2026-08-31

  • 【M】 internal/plugin/cabi/loader.go——CORE_REGISTER_OUTPUT_CH:454的异步 output 从「goroutine 直接返回 queued」改为「goroutine + 带超时 channel 等真实结果」。 新增 awaitOutputResult:276+ 可注入版 awaitOutputResultWith:281+ 常量 outputSendTimeout = 10s
    resCh := make(chan error, 1)
    go func() { resCh <- invoke(pid, channel, argsJSON) }()
    select {
    case err := <-resCh:
        if err != nil { return nil, err }               // 真实失败上报
        return map[string]interface{}{"status": "sent"}, nil
    case <-time.After(timeout):
        return map[string]interface{}{"status": "unconfirmed", "note": "..."}, nil
    }
    
    关键:dev.ExecuteexecuteOutputSendTool 从 Go 侧调起(不在 cgo 栈内goroutine 内的 pluginInvokeOutput 才是 cgo不构成嵌套
  • 【M】 internal/agent/core/output.go executeOutputSendTool:识别 status=unconfirmed|queued → 返回「发送结果未确认:」而非「已发送」,把未确认状态透传给模型。
  • 【R】 无 cgo 嵌套(awaitOutputResult 只在 RegisterOutputChannel 的 handler 内被调用,该 handler 从 Go 侧调起); 「超时未确认」措辞与 11.2 的"已取消"谎言区分——用 unconfirmed + 显式 note不谎报成功也不谎报失败。
  • 【R】 接口冻结:git diff third_party/homeagent-sdk/sdk/ 为空。
  • 【V】 新增 internal/plugin/cabi/output_test.go 三用例全绿:
    • TestAwaitOutputResult_Successstatus=sent
    • TestAwaitOutputResult_Failure(模拟 meta 缺 user_id返回 error(旧实现会谎报成功)
    • TestAwaitOutputResult_Timeoutstatus=unconfirmed 且不返回 error
  • 【V】 go build ./... exit 0go test ./internal/plugin/... ./internal/agent/... 全绿。

0.2 stage lost update 补丁11.3)— 已完成2026-08-31

  • 【M】 templates.goSDK 仓 update 分支 5648519go_invoke_stage 改为 diff 回传:
    • 新增 snapshotWritable(sc) map[string]string——handler 前的序列化快照
    • 新增 changedFieldsOnly(before, after)——只回传变更字段,无变更零回传
    • 第一版踩坑并修正stageContextWritable 返回的切片字段与 sc 共享底层数组handler 原地改元素(sc.ToolResults[0].Result = clean)时 before 快照跟着变diff 看不到变更 → 修复会静默失效。故 before 必须逐字段序列化成字符串。
  • 【M】 internal/plugin/cabi/loader.go applyStageResult 配套(本仓 9bb9cb3tool_calls/tool_results 去掉 len(v)>0 拦截——改为键存在即应用,使插件「清空全部工具调用」的显式 [] 能被表达(旧插件仅 len>0 才带键,不会被误清空)。
  • 【R】 changedFieldsOnly 无竞态(纯函数,无共享状态);只读插件零回传(单测断言)。
  • 【R】 接口冻结:两仓 git diff sdk/ 均为空(只改 bridge 模版 + 内核)。
  • 【R】 bridge 模版可编译性:抽取 tmplLinuxBridge + 真实 weather/plugin.gogo build -buildmode=c-shared → exit 0。
  • 【V】 SDK 仓 tools/plugindev/stagediff_test.go 6 用例全绿:
    • _ReadOnlyPluginReturnsNothing(只读插件零回传——修复核心)
    • _WriterReturnsOnlyChanged(原地改切片元素仅回传 tool_results
    • _ScalarChange / _NewResponseIsReturned / _ClearedSliceIsReturnedAsEmpty
    • _ProductionScenarioNoOverwrite复刻实验 13 现网场景sanitizer 清洗 + weather 只读,清洗结果不再被覆盖)
  • 【V】 内核侧 output_test.go 新增 TestApplyStageResult_ClearedSlicesAreApplied / _OnlyPresentKeysApplied 全绿。
  • 【V】 go build ./... exit 0go test ./internal/plugin/... ./internal/agent/... 全绿。
  • ⚠️ 待部署项:需用新 plugindev 重编全部 17 个外部插件bridge 模版变更),走 plugin_install(overwrite=true)

0.3 reload 语义修正11.6)— ⏭️ 已跳过2026-08-31 用户决策:直接进入进程化重构)

子进程模型下 DF_1_NODELETE 议题整体消失§3.1)——同路径替换 plugin.bin 重启进程即生效。 在 cabi 路径上补 ELF 检测属于「给即将删除的代码打补丁」,性价比低。 现网仍受 reload 假成功影响,但 Part 1 的 entry 分派已为迁移铺路,迁移完成即根治。

  • 【M】dynamic_loader_unix.goELF 检测 DF_1_NODELETE → 标记"不可热重载"。
  • 【M】registry.goReloadOne:对此类插件返回"需重启 homed"。
  • 【M】pluginmgr/plugin.goplugin_install:返回 restart_required 替代 reload_required
  • 【R】确认 .so 插件重载不再"假成功"。
  • 【V】单测mock ELF 头带 NODELETE vs 不带 → 正确区分。

0.4 超时日志措辞修正 + 附带11.2 短期项 + 11.4)— ⏭️ 已跳过(同上)

11.2 的 cgo 超时不可中断在子进程模型下由 Process.Kill() 真正解决§9.5 11.4 的 Lua 路径在迁移后统一走 RPC三套 ABI 收敛),锁语义天然有边界。

  • 【M】internal/agent/core/toolcall.go:41:日志从"已取消"改为"已放弃等待(插件仍在后台运行,其占用的线程无法回收)"。
  • 【M】internal/plugin/lua_plugin.go:726stage 快照加 sc.RLock()/RUnlock()11.4)。
  • 【R】措辞语义诚实Lua 快照持锁。
  • 【V】make test 全绿;超时日志不再撒谎。

Part 0 出口条件11.1/11.3/11.6 全部落地并有针对性测试;生产可先部署(现网止血)。


Part 1加载分派骨架阶段 2.4S

依据:迁移评估 §2.4 / 3.2plan.md 11.7。目标:让 registry 能按 entry 把插件分派到 .socabi.binproc两条通道——双通道共存是整个计划可回退的前提

修改(核心)

  • 【M】internal/plugin/manifest.goPluginManifest.Entry 注释与 IsPluginDir 支持 plugin.bin
  • 【M】internal/plugin/dynamic.go:新增 binEntry = "plugin.bin" 常量;readManifest 读取 entry。
  • 【M】internal/plugin/registry.go loadOne~:376把「无工厂 → tryDynamic」的分支改为按 entry 分派:
    switch entry {
    case soEntry, dllEntry:  p, err = r.tryLoadSO(...)   // 现有 cabi
    case binEntry:           p, err = r.tryLoadProc(...) // 新增Part 2 填充)
    default:                 p, err = r.tryOther(...)    // lua / skill
    }
    
    先保留一个 tryLoadProc 桩(返回"未实现"错误),保证分派骨架先成立、可测。
  • 【M】internal/plugin/dynamic_loader_unix.go:把 tryLoadSOtryDynamic 拆出成 registry 可独立调用的函数。

审查

  • 【R】确认内置插件hasFactory 分支)完全不受影响——仍走 RegisterNative 进程内路径。
  • 【R】确认 .so 路径行为与今天逐字节一致(无回归)。
  • 【R】接口冻结git diff 公开 SDK 为空。

验证

  • 【V】单元测试mock 三种 manifestso/dll/bin/lua→ 分派到正确通道;.bin 桩返回明确错误而非 panic。
  • 【V】既有 .so 插件加载 e2e 不回归(带一个真实 .so 冒烟)。

Part 1 出口条件:分派骨架在,.bin 有明确桩位,.so 全回归。

Part 1 已完成2026-08-31commit 610e9d0

  • 【M】 dynamic.go:新增 binEntry/skillEntry 常量 + entryKind 枚举 + classifyEntry / detectEntryKind
    • manifest 的 entry 优先级最高——把 entry 改回 plugin.so 即回退 cabi 通道(回退路径的保证)
    • 无 manifest 时按目录探测,.bin 优先于 .so(迁移期同目录两产物共存时走新通道)
  • 【M】 registry.go tryDynamic:按 entry 分派 proc/cabientry 声明 .bin 但二进制缺失时报明确错误,不静默回退
  • 【M】 registry.go pluginEntryHash:候选顺序与 detectEntryKind 对齐(.bin 优先),否则增量重载会用错文件算 hash
  • 【M】 manifest.goEntry 字段注释补 plugin.bin
  • 【M】 dynamic_proc_unix.go / dynamic_proc_windows.gotryLoadProc 桩位(存在性/类型/可执行权限校验已实现)
  • 【R】 内置插件(hasFactory 分支)完全未受影响——仍走进程内 RegisterNative
  • 【R】 .so 路径行为与改动前一致(既有测试全绿,无回归)
  • 【R】 接口冻结:git diff third_party/homeagent-sdk/sdk/ 为空
  • 【V】 entry_dispatch_test.go 9 项全绿:
    • TestClassifyEntry8 种 entry 分类)
    • TestDetectEntryKind_ManifestWins / _ManifestCanForceRollback回退路径验证
    • TestDetectEntryKind_ProbeOrderPrefersBin / _ProbeFallbacks4 子例)
    • TestTryLoadProc_MissingBinaryReturnsNil / _NonExecutableRejected
    • TestPluginEntryHash_PrefersBin / _EmptyForFactoryOnlyPlugin
  • 【V】 go build ./... exit 0go test -race ./internal/plugin/... 全绿;全量 32 个包测试通过

Part 2子进程通道原型阶段 2.1~2.3/2.5/2.9~3 周,核心风险点)

依据:迁移评估 §4.1 阶段 2迁移评估指明可大幅参考 clawhubadapter/sidecar.go:54-350(已有 stdin/stdout + pending map + notifyCh。 目标把单个外部插件weatherplugin.bin 端到端跑通,验证"接口不变"假设。

修改(核心)

  • 【M】新建 internal/plugin/proc/
    • process.go——procPlugin 实现 sdk.Plugin 接口;spawn/健康检查/优雅停止/Close()=真 kill+wait。
    • 可参考 clawhubadapter/sidecarProcessexec.Cmd + stdin *bufio.Writer + readLoopscanner 大 buffer 64KB+ pending map[int]chan<- []byte + notifyCh chan OCNotification + readerStop/readerWg。
    • rpc.go——双向 JSON-RPC 编解码7 个 kernel→plugin 调用(tool.invoke/stage.invoke/output.invoke+ 51 个 plugin→kernel 回调(平移自合同面 B 映射表)。
  • 【M】internal/plugin/dynamic_loader_unix.go:实现 tryLoadProcspawn .bin,回连 stdio RPC
  • 【M】internal/plugin/registry.go closePlugin/卸载路径:对 proc 插件 Close() 真 kill。
  • 【M】internal/agent/core/plugin_health.go 调用侧:插件退出码/EOFrecordCrash逻辑完全复用,仅把"panic 捕获"换成"进程退出检测",见迁移评估 §2.3)。

审查

  • 【R】readLoop 鉴权:只接受来自本进程 spawn 的 stdout防注入
  • 【R】JSON-RPC 帧边界处理(bufio.Scanner 长行截断风险——沿用 sidecar 64KB buffer
  • 【R】pending map 泄漏:超时清 map、退出时清 map。
  • 【R】崩溃重启SetAutoRestart(true) 语义保留;plugin_health 冷却/自愈复用。
  • 【R】接口冻结公开 SDK 零 diff。

验证

  • 【V】单测spawn→握手→工具调用往返→正常 Stop→kill 崩溃→退出码捕获。
  • 【V】weather .bin 端到端:RegisterTool/Settings/InjectInputSync 全部经 stdio RPC 打通。
  • 【V】与 Part 1 的 entry 分派联动:同目录 .so.bin 共存互不干扰。

Part 2 出口条件:一个真实外部插件 .bin 全链路可用,崩溃隔离生效,接口零改动。

Part 2 已完成2026-09-01commit d62430a + 82dcc86

  • proc/protocol.goNDJSON 帧、51 个 method id 平移为 method 名(编号扔掉)、握手/stage/tool/output 参数类型。 case 25(CORE_FREE_STRING) 无对应 methodGC 接管);case 23/24(事件订阅) 与 io.setToolBlocks 明确返回未实现,不静默成功
  • proc/process.goSpawn/readLoop/CallContext/Notify/Stop/Kill/markExited单帧上限 1MB。
  • proc/corehandler.go51 case 平移 + CoreSDK 接口(刻意排除内核内部机制,见 Part 6 权限梯度)。
  • proc/host.go全部插件共享同一 memfd。最初写成每插件一块段,尝试后发现 那等于副本模型换壳(各写各段、各自回读、最后回读者覆盖前者),已改正。
  • proc/stage.goRunStage 接线 + lockRegistryproc/plugin.goPlugin 实体。
  • 共享段分配按平台拆分(shmalloc_linux.go memfd / shmalloc_darwin.go 立即 unlink 的临时文件 / shmalloc_other.go 明确报错)——不静默降级成「无共享段」,那会让 stage 静默失去数据面。
  • registry 接线commit 11c1bbctryDynamicRegistry.loadProcHost 惰创建且全局唯一; StopAll 锁外释放共享段(插件还持有映射时拆段 → SIGBUS持锁调与 onProcCrash 有锁序风险); onProcCrash 只发 EventSystem 事件,不在回调里直接重载(重载需 registry 锁)。
  • proc_core.go —— 权限梯度的类型系统落点:procCore命名字段持有 *isdk.PluginSDK 不是嵌入。嵌入会提升全部方法,外部插件就能经类型断言拿到 Supervisor/Tracker/Adapter/Indexer/Status/Selftest。
  • 测试 36 项含 -racetestdata/ 8 个假插件 + e2e_template_test.go真实 plugindev 模板 编译插件跑全链路(验证「模板 ↔ 内核」协议/布局真的对齐,不只是内核自己跟自己对齐)。

Part 3plugindev 工具链改造(阶段 2.6/2.7/2.8MSDK 仓)

依据:合同面 B迁移评估 §4.1。此部分在独立 SDK 仓维护(用户决策 sdk_repo_only。 目标:让外部插件能用普通 go build 产出 .bin,业务代码零改动。

修改(工具链)

  • 【M】tools/plugindev/templates.go:新增 tmplProcMain——把 bridge 从「7 个 //export + -buildmode=c-shared」改为「main() + stdio JSON-RPC loop」注册逻辑buildPluginSDK 的 registar 闭包)从 callVoid(id,...) 改为 sendRPC(methodName,...)(合同面 B 的平移)。
  • 【M】tools/plugindev/cmd_build.go
    • 新增目标 plugin.bingo build(去 -buildmode=c-sharedCGO_ENABLED=0)→ plugin.bin
    • bundle 平台表:{"linux/amd64","plugin.bin"}(替代 .so)。
    • resolveBuildbin 分支不再需 C 编译器。
  • 【M】tools/plugindev/cmd_build.go validBinaries/打包:.hmap 内条目支持 plugin.binplugin.json entry 写 plugin.bin)。
  • 【M】plg.json 模板(tmplPlgJSONentry 默认改为 plugin.bin(保留 .so 兼容)。

审查

  • 【R】生成的 tmplProcMain 与旧 bridge 的 SDK 方法一一对应(对照合同面 B 51 行映射表逐行核对)。
  • 【R】业务代码零改动证据:同一 plugin.go,仅入口文件/构建命令不同。
  • 【R】交叉编译简化确认.bin 无需 cgo 工具链,跨 GOOS 仅需目标 toolchain。

验证

  • 【V】用新 plugindev 重编 example/weather → 产出 plugin.bin
  • 【V】.hmap 打包/解包校验:plugin.bin 条目正确登记。
  • 【V】与 Part 2 集成weather.bin 被 homed proc 通道正确加载运行。

Part 3 出口条件plugindev 一条命令产出 .bin + 正确 .hmap,外部插件源码零改动。

Part 3 已完成2026-09-02SDK 仓 commit 09b64dc

模板落地方式换了:不是计划里的 templates.go 新增 tmplProcMain raw string 而是真实 .go 源文件 templates/proc_main.go.tmpl + //go:embedproc_runtime.go)。 原因900+ 行代码塞在字符串里写错只能等生成插件时才炸,作为源文件可被 go/parsergofmtgo vet 直接检查。这也是 proc_runtime_test.go 16 项 静态检查得以存在的前提。

  • templates/proc_main.go.tmpl1113 行51 个 method 的插件侧 RPC 实现 procIO/procMemory/procSettings/procSocial/procLLM/procKnowledge/ procDocMemory/procTextMemory/procPluginMgr、共享段访问fd 3与 16 字段 StageContext 编解码、handleStageInvoke(拿锁 → 读段 → handler → 只写脏字段 → 放锁)。
  • cmd_build.goresolveBuild(target, proc) 分派proc 走 go build -trimpath + CGO_ENABLED=0 交叉编译不再需要目标平台 C 工具链。bundle 模式各平台产物同名(进程边界即 ABI 边界, 无平台扩展名),故 zip 内加平台后缀 plugin.bin.linux.amd64
  • proc_runtime.go:生成时清理残留 z_bridge_gen.go/z_entry.c——同目录两套 main 会编译冲突, 这让 .so.bin 切换无需人工清理。

计划外补的一个真缺口lifecycle.autoRestart 没接线。公开 SDK 的 SetAutoRestart 是纯 setters.autoRestart = enabled,无回调 hook。C ABI 下内核在 Start 返回后 直接读 plgSDK.AutoRestart();子进程隔着进程边界读不到,插件调它只改自己进程内的副本。 修法:模板在 plg.Start() 返回后显式上报一次(内核侧 corehandler.go:145 早已就绪)。 没有改公开 SDK 接口

验证(均已实测):

$ plugindev build              # plg.json: entry = "plugin.bin"
  compiling linux/amd64 (子进程模式CGO_ENABLED=0)...
  packaged weather_linux_amd64.hmap

build/plugin.bin  →  ELF 64-bit executable, statically linked   ← 零 cgo
dist/*.hmap       →  plugin.json + plugin.bin

$ diff example/weather/plugin.go <构建目录>/plugin.go
✅ 逐字节一致                    ← 业务代码零改动的硬证据

$ git diff third_party/homeagent-sdk/sdk/
(空)                            ← 接口冻结保持

Part 4共享内存数据面阶段 3.1~3.5~3 周,最高风险)

依据:迁移评估 §3.3 数据面 / 3.4 SDK 封装 / 3.7 锁仲裁;合同面 C。 目标:多插件并发改写同一 StageContext 语义与今天一致(丢失率 → 0外部插件看到全部 16 字段。

修改

  • 【M】internal/plugin/proc/ 新增 shm.go
    • 共享段 schemaShmStageCtx + Slice{off,len} 偏移描述符 + arenaappend-only + 压实)。
    • arena 分配器:插件把 FinalText 从 10B 改 10KB 时分配新区域、旧区域留垃圾、stage 结束后压实。
    • 4 个 Extramedia_blocks/media_type/input_source/output_channel提升为具名字段迁移评估 §3.3 已核实全部使用点)。
    • 段生命周期:创建/挂载/插件崩溃后清理。
  • 【M】internal/plugin/proc/shmcodec.goStageContext ↔ 共享段编解码偏移↔Go 值转换)。
  • 【M】internal/plugin/proc/lock.go锁仲裁 RPC——插件 Lock/RLockstage.lock/stage.unlock → 内核 sync.Mutex 排队(迁移评估 §3.7 已裁定,实验 3+9 支撑)。
  • 【M】internal/agent/core/stages.go RunStage:改造为跨进程并发扇出(保留并发语义,最难一环)——内置插件仍进程内 go func,外部插件走共享段 + 锁仲裁。
  • 【M】SDK 侧(插件进程内)封装全部复杂度(迁移评估 §3.4):插件保留原生 StageContexthandler 照常读写,脏字段写回共享段。

审查(最高优先级 review

  • 【R】并发语义一致性内置0% 丢失)与外置(迁移前 35.8~36.8%)在共享内存下都收敛到 0% 丢失。
  • 【R】锁仲裁死锁持锁进程崩溃自愈实验 9 已证无需 robust mutex
  • 【R】arena 单 stage 写入上限:大写入在 SDK 层报错而非静默截断(迁移评估 §4.4)。
  • 【R】Extra 不引入通用 tagged union 成本(维持 4 键具名字段)。
  • 【R】接口冻结sdk/ 零 diffStageContext 结构体字段序不变。

验证

  • 【V】复刻实验 85 子进程 × 300 轮并发改写 → 零丢失零撕裂
  • 【V】复刻实验 13 现网场景sanitizer改 ToolResults+ weather只读并发 → 清洗结果不再被覆盖。
  • 【V】改写型插件行为基线测试sanitizer/multimodal 迁移前后行为对拍(迁移评估 §4.4 风险缓解)。

Part 4 出口条件:跨进程并发改写零丢失,内置/外置语义一致16 字段全可见。

Part 4 核心已完成2026-08-31commit 610e9d0)—— 段 / 编解码 / 锁仲裁三件套

用户明确指出「基于共享内存的 stage 并发是最为关键的」,故先于 Part 2/3 落地数据面。 RunStage 的跨进程接线3.4)待 Part 2 的进程通道就绪后进行。

  • 【M】 proc/shm.go 段布局与 arena 分配器§3.3
    • Header(64B) + ShmStageCtx(描述符数组 + 标志位) + append-only arena
    • 相对偏移:各进程 mmap 到不同虚拟地址仍能正确解引用
    • NewSegment / AttachSegment 带魔数 + 版本校验(版本不匹配显式报错,不静默错读)
    • arena 用尽显式报错而非静默截断§4.4 风险登记的硬要求)
    • Compact() 回收 append-only 垃圾,须在无插件持锁时调用
  • 【M】 proc/shmcodec.go StageContext 16 字段跨进程编解码§3.4
    • 字段级描述符消除 lost update:只改 FinalText 的插件完全不触碰 ToolResults 描述符
    • WriteDirty 只写脏字段——只读插件零写入,不可能覆盖他人改写
    • Snapshot序列化字符串切片共享底层数组的坑C ABI 侧修 11.3 时已踩过一次)
    • Extra 4 键提升为具名字段;Response 用标志位区分 nil 与空串(短路语义)
    • 全 16 字段可见——今日经 C ABI 只有 10 个,ContextMsgs/ReasoningContent/TokenUsage/Memory/Extra/Errors 首次对外部插件可见
  • 【M】 proc/lock.go 锁仲裁回归内核§3.7 已裁定,零 cgo
    • ForceRelease 实现实验 9 的崩溃自愈 → 排除 robust pthread_mutex 必要性
    • 重复加锁显式拒绝(否则死锁 30s比挂死更难排查
    • 等待超时有补偿 goroutine 防锁永久泄漏
  • 【R】 并发语义:TestSegment_ConcurrentAppend_NoLostUpdate 断言「各标记计数之和 == 最终长度 且 == 期望写入次数」,同时排除丢失与撕裂
  • 【R】 arena 上限报错(非静默截断):TestSegment_ArenaExhaustionReturnsError
  • 【R】 Extra 维持 4 键具名字段,未引入通用 tagged union 成本
  • 【R】 接口冻结:sdk/ 零 diffStageContext 结构体未改
  • 【R】 go vet 干净(含 copylocks 检查)
  • 【V】 proc 包共享段部分 16 项测试全绿(含 -race(全包现 36 项,含进程/端到端):
    • 段:魔数/版本校验、全 16 字段往返、Response nil vs 空串
    • 脏字段:只读零写回、原地改切片被识别、压实不破坏字段
    • 现网场景复刻TestSegment_ProductionScenario_SanitizerNotOverwrittenByWeathersanitizer 清洗 + weather 只读并发,清洗结果不被覆盖)
    • 并发零丢失5 插件 × 40 轮读-改-写同一字段200 次写入全部保留
    • 锁:互斥、串扰拒绝、未持锁释放拒绝、重复加锁拒绝、崩溃自愈、定向强制释放、临界区串行化

Part 4 RunStage 接线已完成2026-09-0109-02

  • proc/stage.go 把内核 RunStage 的并发扇出接到共享段: Host.beginStage(首个到达者独占段并写入 StageContextstage.invoke RPC → 插件侧 stage.lock → 读段 → handler → 只写脏字段 → stage.unlockHost.endStage(最后离开者回读 + 压实 arena
  • 并发扇出保留§0.2 第 1 条:并发扇出是原始设计,不是缺陷); stageMu 串行化整次 stage 对共享段的独占(内核可能在不同路径并发触发 RunStage而段只有一份
  • 端到端验证(e2e_template_test.go,用真实 plugindev 模板编译的插件, 而非 testdata/ 手写假插件——后者只能验证内核自己跟自己对齐):
    • TestE2E_RealTemplatePluginFullLifecycle:握手 → init/start → 反向注册 → 工具调用 → stage 读改写;同时验证 FinalText 回传 C ABI 下 after_toolcall 看不到此字段§8.3 10→16
    • TestE2E_RealTemplateReadOnlyPluginDoesNotOverwrite:两插件共享同一 Host 并发, 只读插件不覆盖改写插件的结果(若每插件一块段,此测试必然失败)

Part 4 已整体完成


Part 5通知面阶段 4.1~4.5~1.5 周)

依据:迁移评估 §3.6 事件环 / §2.4 约束 B / §3.8。目标:外部插件首次获得事件订阅能力,且不阻塞流式输出。

修改

  • 【M】internal/plugin/proc/eventring.goEvtRing + Subscriber schemawrite_seq/read_seq/dropped/type_mask/last_seen溢出计数、允许丢但让消费者知道丢了。
  • 【M】eventfd 通知 + Go netpoller 消费:unix.Eventfd(EFD_NONBLOCK|EFD_CLOEXEC) + os.NewFile 注册 netpoller不占 OS 线程——实验 1 已证 200 goroutine 仅 +1 线程)。
  • 【M】internal/events/bus.go Publish:加事件环投递(post-and-forget绝不等待消费者,满足约束 B
  • 【M】实现 case 23/24(今天空实现)——Events().Subscribe 对外部插件真正可用。
  • 【M】订阅者活性检测last_seen 超时 → recordCrash

审查

  • 【R】Bus.Publish 路径禁用任何锁/阻塞——流式输出逐 token 发布,任何等待都会卡顿(迁移评估 §4.3 风险高)。
  • 【R】溢出语义drops 计数暴露,不静默丢。
  • 【R】eventfd 计数合并1000 token 事件只唤醒几次。

验证

  • 【V】流式压测长回复下 Publish 单次耗时不随订阅者数线性恶化。
  • 【V】复刻实验 4post-and-forget 解耦5s → 2.3ms 量级)。
  • 【V】外部插件订阅事件端到端原空实现 case 23/24 现在可用)。

Part 5 出口条件:事件订阅对外可用,流式输出无卡顿。


Part 6迁移与收尾阶段 5.1~5.4~2 周)

依据:迁移评估 §4.5 双通道共存、§5 权限梯度。逐插件迁移,随时回退。

修改

  • 【M】17 个外部插件逐个用新 plugindev 重编为 .binplugin_install(overwrite=true)),每个回归验证。
  • 【M】plugins/ 目录逐个把 entryplugin.so 改为 plugin.bin
  • 【M】删除 internal/plugin/cabi/1096 行)+ bridge 模板 tmplLinuxBridge/tmplBridge385 行)+ dynamic_dll_*/dynamic_loader_windows.go
  • 【M】权限梯度显式化manifest 声明 caps + 内核侧白名单(Selftest/Supervisor/Tracker/Status/Adapter/Config/Tool/Indexer/OutputChan/Publish 确认不给)。
  • 【M】lua_plugin.go/dynamic_lua.go:统一走 RPC收敛三套 ABI 为单一 RPC
  • 【M】文档PLUGIN_DEV.md 更新、迁移说明。

审查

  • 【R】每删一个 cabi 依赖项,go build ./... + go vet ./... 干净。
  • 【R】权限梯度外部插件无权访问的 API 在 RPC 边界被拒绝(非忽略)。
  • 【R】接口冻结sdk/ 零 diff。

验证(全量回归)

  • 【V】17 插件每个 .bin 独立回归(工具/设置/通道/阶段)。
  • 【V】.so.bin 混跑集群冒烟Part 1 分派 + 双通道共存)。
  • 【V】make test 全量绿 + go build ./...
  • 【V】内存/RSS 对比:迁移后常驻 ≤ 基线 +29MB实验 5 量级)。
  • 【V】工具调用 RPC 延迟 p50 ≤ 20µs 量级(实验 11

Part 6 出口条件:全部外部插件 .bincabi 删除,接口零改动,权限显式化,无回归。


最终验收清单(对照接口不变矩阵 §7 检查点)

# 检查点 通过标准
1 公开 SDK 接口冻结 git diff third_party/homeagent-sdk/sdk/ 为空(全程)
2 外部插件业务代码零改动 17 个 example/*/plugin.go 与基线逐字节可比
3 17 插件 .bin 全部经 plugin_install 加载,工具/设置/通道/阶段 e2e
4 cabi 删除 internal/plugin/cabi/ 与 bridge 模板不存在
5 崩溃隔离 插件 kill 只退出自身homed 存活
6 热重载 同路径换 .bin 即生效,无需重启
7 并发改写 跨进程 stage 丢失率 0%(对照今天 35.8~36.8%
8 事件订阅 外部插件 Events().Subscribe 可用
9 多模态 SetToolBlocks 非空实现
10 超时取消 工具超时可 Process.Kill(),零泄漏
11 output_send 真实结果返回(非假成功)
12 权限梯度 内部专属 API 在 RPC 边界拒绝
13 内存/延迟 常驻 +≤29MBRPC p50 ≤20µs 量级

风险与回退

风险 缓解 回退
Part 2/4 RunStage 并发语义漂移 复刻实验 8/13 + sanitizer/multimodal 对拍Part 4 review entry 分派切回 .soPart 1 双通道)
Part 4 Bus.Publish 阻塞卡顿 专项流式压测Part 5 事件环投递后置,先降级进程内
Part 3 工具链 .bin 产物问题 单插件 weather 先行验证 保留 .so 构建分支
Part 6 17 插件回归 逐个迁移 + plugin_install(overwrite) 任意一个失败立即回退该插件 entry
接口意外漂移 每部分【R】强制 git diff sdk/ 检查 立即 revert暴露合同面违约

规划2026-08-31update 分支。Part 编号与其依赖的 plan.md/迁移评估阶段对应。