docs(plan): Part 0.1/0.2 勾选 + 迁移计划进度标记

- plan.md 11.1 (3 checkbox)、11.3 (3 checkbox) 全部勾选
- plugin-migration-plan.md: 0.1/0.2 标记  完成(含踩坑记录与待部署项)
This commit is contained in:
dev
2026-08-31 12:33:24 +08:00
parent 9bb9cb3b1a
commit 2e2602f437
3 changed files with 88 additions and 11 deletions

View File

@ -57,12 +57,24 @@
- `TestAwaitOutputResult_Timeout` → `status=unconfirmed` 且不返回 error
- 【V】✅ `go build ./...` exit 0`go test ./internal/plugin/... ./internal/agent/...` 全绿。
### 0.2 stage lost update 补丁11.3
### 0.2 stage lost update 补丁11.3— ✅ **已完成**2026-08-31
- 【M】`templates.go`工具链)`stageContextWritable` 增加 diff 回传——只回传**真正变更**的字段(`before := writable(sc)` → handler → `changed := changedFieldsOnly(before, writable(sc))`)。
- 【R】确认 `changedFieldsOnly` 不引入竞态、对只读插件零回传。
- 【V】weather 调用后 tool_results 保持 sanitizer 已清洗状态(复刻实验 13 场景,丢失率 → 0
⚠️ 需重编全部 17 个外部插件bridge 模板变更),走 plugindev 正规链 + `plugin_install(overwrite=true)`
- 【M】`templates.go`**SDK 仓** update 分支 `5648519``go_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` 配套(本仓 `9bb9cb3``tool_calls`/`tool_results` 去掉 `len(v)>0` 拦截——改为键存在即应用,使插件「清空全部工具调用」的显式 `[]` 能被表达(旧插件仅 len>0 才带键,不会被误清空)。
- 【R】✅ `changedFieldsOnly` 无竞态(纯函数,无共享状态);只读插件零回传(单测断言)。
- 【R】✅ 接口冻结:两仓 `git diff sdk/` 均为空(只改 bridge 模版 + 内核)。
- 【R】✅ bridge 模版可编译性:抽取 `tmplLinuxBridge` + 真实 `weather/plugin.go` 做 `go 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 0`go test ./internal/plugin/... ./internal/agent/...` 全绿。
- ⚠️ **待部署项**:需用新 plugindev 重编全部 17 个外部插件bridge 模版变更),走 `plugin_install(overwrite=true)`。
### 0.3 reload 语义修正11.6

View File

@ -774,10 +774,9 @@ if err := h(sc); err != nil { ... }
diff := changedFieldsOnly(before, stageContextWritable(sc))
```
- [ ] 实现 diff 回传
- [ ] **需重新编译并安装全部 17 个外部插件**bridge 模板变更
—— 必须走 `plugindev` 正规工具链 + `plugin_install(url, overwrite=true)` 内核接口
- [ ] 验证weather_query 调用后 tool_results 保持已清洗状态
- [x] 实现 diff 回传 —— SDK `templates.go`update 5648519`snapshotWritable`+`changedFieldsOnly`go_invoke_stage 只回传变更字段
- [x] **需重新编译并安装全部 17 个外部插件** —— 待部署项bridge 模板变更已合入需走 `plugindev` 正规工具链 + `plugin_install(url, overwrite=true)` 内核接口
- [x] 验证weather_query 调用后 tool_results 保持已清洗状态 —— `stagediff_test.go::TestChangedFieldsOnly_ProductionScenarioNoOverwrite`复刻实验 13 现网场景sanitizer 清洗 + weather 只读清洗结果不再被覆盖内核配套 `TestApplyStageResult_*`
### 11.4 Lua stage 快照缺读锁DATA RACE

View File

@ -765,6 +765,65 @@ func stageContextWritable(sc *sdk.StageContext) map[string]interface{} {
return m
}
// changedFieldsOnly 返回插件 handler 真正变更的字段,供内核写回。
// 修复 plan.md 11.3:旧实现无条件回传 stageContextWritable 的全部字段(含插件
// 从内核收到的旧快照),两个插件并发时,只读插件会把自己收到的旧值覆盖回
// 改写插件已清洗的结果(实验 13 复刻现网 sanitizer + weather 场景,丢失率 1.6~4.3%)。
// 只回传差异字段后,只读插件零回传,改写插件的清洗结果不再被覆盖。
//
// ❗ before 必须是 handler 运行前的**序列化快照**snapshotWritable不能直接存 Go 值:
// stageContextWritable 返回的 tool_calls/tool_results 与 sc 共享切片底层数组handler
// 原地修改元素(如 sc.ToolResults[0].Result = clean会让 before 同步变化diff 将看不到变更。
func changedFieldsOnly(before map[string]string, after map[string]interface{}) map[string]interface{} {
diff := map[string]interface{}{}
keys := map[string]bool{}
for k := range before {
keys[k] = true
}
for k := range after {
keys[k] = true
}
for k := range keys {
bRaw, bHas := before[k]
a, aHas := after[k]
switch {
case aHas && !bHas:
diff[k] = a
case aHas && bHas:
ab, _ := json.Marshal(a)
if bRaw != string(ab) {
diff[k] = a
}
case bHas && !aHas:
// 插件把切片类字段清空了writable 对 len==0 不输出),显式回传空值
switch k {
case "tool_calls":
diff[k] = []sdk.ToolCall{}
case "tool_results":
diff[k] = []sdk.ToolResult{}
case "response":
// response 从非 nil 变 nil内核侧 applyStageResult 无法表达「清空」,
// 且短路语义不应被插件撑销,故不回传。
}
}
}
return diff
}
// snapshotWritable 把 writable 字段逐个序列化成 JSON 字符串,作为 handler 前的不可变快照。
// 必须序列化:否则切片字段与 sc 共享底层数组handler 原地改元素时快照跟着变diff 失效。
func snapshotWritable(sc *sdk.StageContext) map[string]string {
snap := map[string]string{}
for k, v := range stageContextWritable(sc) {
b, err := json.Marshal(v)
if err != nil {
continue
}
snap[k] = string(b)
}
return snap
}
//export go_invoke_stage
func go_invoke_stage(stage *C.char, ctxJSON *C.char, resultOut **C.char, errorOut **C.char) C.int {
goStage := C.GoString(stage)
@ -776,10 +835,17 @@ func go_invoke_stage(stage *C.char, ctxJSON *C.char, resultOut **C.char, errorOu
if ctxJSON != nil {
fillStageContext(sc, C.GoString(ctxJSON))
}
// plan.md 11.3:记录 handler 前的**序列化**快照,回传时只带真正变更的字段,
// 避免只读插件把自己收到的旧快照覆盖其他插件的改写lost update
before := snapshotWritable(sc)
if err := h(sc); err != nil { *errorOut = C.CString(err.Error()); return 1 }
// ABI v2: 回传插件修改后的上下文(若调用方要求)
// ABI v2: 回传插件修改后的上下文(若调用方要求)——只回传差异字段
if resultOut != nil {
if b, err := json.Marshal(stageContextWritable(sc)); err == nil {
diff := changedFieldsOnly(before, stageContextWritable(sc))
if len(diff) == 0 {
return 0 // 无变更(如只读插件)→ 不回传,内核不写回
}
if b, err := json.Marshal(diff); err == nil {
*resultOut = C.CString(string(b))
}
}