Files
HomeAgent/internal/plugin/proc/testdata/appendplugin.go
JianFeeeee 50ba4a64cb feat(shm): 文档/知识正文入共享内存 + 协议版本 bump 到 2(§13.13)
## 数据面补齐

- doc.insert / doc.insertWithMedia:新增 doc_ref / attachments_ref,
  模板序列化后 putValueInArena
- knowledge.add:新增 content_ref(内容是 JSON 字符串,读出后再解一层)
- 抽出通用 resolveJSONRef(resolveBlocks 也改用它),三处共用一套
  “共享优先、内联回退”逻辑

## 协议版本 bump:让错配显式失败,而不是静默坏

这是本轮更重要的部分。§13.6/§13.13 改了内核→插件 payload 的承载方式,
两种错配都不会报错、只会静默失效:

- v1 插件只读内联 args(tool/cleaner/output)→ 遇到 v2 内核拿到空参数
- v2 插件发 blocks_ref → v1 内核反序列化时静默忽略(旧内核
  io.setToolBlocks 还是桩实现)

现场表现为“输出变空 / 图注入没反应”,极难定位。所以把 ProtocolVersion
与模板 procProtocolVersion 一起 bump 到 2:双方都是等值校验,v1 插件遇上
v2 内核会在建链时明确报“协议版本不匹配…请用配套 plugindev 重编”。

测试里把“错误必须带出重编指令”也断言上了——生产上碰到它的现场就是
“只更新了内核没重编插件”,光报“不匹配”定位不到行动。

testdata 8 个插件的 protocol 同步更新(badprotoplugin 仍用 999 验证拒绝)。
工具链已重建并安装(协议 2,内嵌 blocks_ref/doc_ref/content_ref),
回滚副本 plugindev.bak-20260910-232544。

验证:-race 全绿。新增 KnowledgeAddViaArena(12000B 正文)、
KnowledgeAddInline、DocInsertViaArena、SetToolBlocks 三例 +
真实模板 e2e + 协议不匹配断言强化。

§13.13 第 5 条(反向大结果)范围更大——需把内核→插件的应答路径整体改成
“大结果写段 + 返回 ref”,涉及 callCore 的返回处理与 doc.query/llm.chat 等
所有读大结果的 method。已在 plan.md 标注未做,不冒充完成。
2026-09-10 23:26:16 +08:00

236 lines
5.4 KiB
Go
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.

//go:build ignore
// appendplugin 在 stage 中把自己的标记追加到 FinalText读-改-写)。
//
// 用于跨进程 lost update 验证:多个此类插件并发处理同一 stage
// 若全部标记都保留 ⇒ 无丢失;若少了 ⇒ 出现 lost update。
//
// 这是实验 85 进程 × 300 轮零丢失)在真实 RPC + 真实内核 RunStage
// 下的复刻——机制单测已过,这里验证集成后同样成立。
package main
import (
"bufio"
"encoding/binary"
"encoding/json"
"fmt"
"os"
"sync"
"syscall"
)
type request struct {
ID uint64 `json:"id,omitempty"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
type response struct {
ID uint64 `json:"id"`
Result interface{} `json:"result,omitempty"`
Error string `json:"error,omitempty"`
}
var (
out = bufio.NewWriter(os.Stdout)
writeMu sync.Mutex
nextID uint64
pendMu sync.Mutex
pending = map[uint64]chan json.RawMessage{}
shm []byte
tag string
)
func send(v interface{}) {
b, _ := json.Marshal(v)
writeMu.Lock()
out.Write(b)
out.WriteByte('\n')
out.Flush()
writeMu.Unlock()
}
func callKernel(method string, params interface{}) (json.RawMessage, bool) {
pendMu.Lock()
nextID++
id := nextID
ch := make(chan json.RawMessage, 1)
pending[id] = ch
pendMu.Unlock()
var raw json.RawMessage
if params != nil {
b, _ := json.Marshal(params)
raw = b
}
send(request{ID: id, Method: method, Params: raw})
r, ok := <-ch
return r, ok
}
const (
headerSize = 64
stageFieldCount = 18
sliceSize = 8
flagCount = 8
offArenaBase = 8
offArenaCap = 12
offArenaUsed = 16
offCtxBase = 20
offSeq = 24
fFinalText = 5 // 与 proc/shmcodec.go 的 stageField 枚举顺序一致
)
func desc(field int) (uint32, uint32) {
cb := binary.LittleEndian.Uint32(shm[offCtxBase:])
o := cb + uint32(field*sliceSize)
return binary.LittleEndian.Uint32(shm[o:]), binary.LittleEndian.Uint32(shm[o+4:])
}
func setDesc(field int, off, ln uint32) {
cb := binary.LittleEndian.Uint32(shm[offCtxBase:])
o := cb + uint32(field*sliceSize)
binary.LittleEndian.PutUint32(shm[o:], off)
binary.LittleEndian.PutUint32(shm[o+4:], ln)
}
func readFinalText() string {
off, ln := desc(fFinalText)
if off == 0 && ln == 0 {
return ""
}
if ln == 0 {
return ""
}
base := binary.LittleEndian.Uint32(shm[offArenaBase:])
return string(shm[base+off : base+off+ln])
}
func writeFinalText(s string) error {
used := binary.LittleEndian.Uint32(shm[offArenaUsed:])
if used == 0 {
used = 1
}
cap_ := binary.LittleEndian.Uint32(shm[offArenaCap:])
if used+uint32(len(s)) > cap_ {
return fmt.Errorf("arena 空间不足")
}
base := binary.LittleEndian.Uint32(shm[offArenaBase:])
copy(shm[base+used:], []byte(s))
binary.LittleEndian.PutUint32(shm[offArenaUsed:], used+uint32(len(s)))
setDesc(fFinalText, used, uint32(len(s)))
// 世代号自增
v := binary.LittleEndian.Uint64(shm[offSeq:])
binary.LittleEndian.PutUint64(shm[offSeq:], v+1)
return nil
}
func main() {
tag = os.Getenv("PLUGIN_TAG")
if tag == "" {
tag = "?"
}
in := bufio.NewScanner(bufio.NewReader(os.Stdin))
in.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for in.Scan() {
line := make([]byte, len(in.Bytes()))
copy(line, in.Bytes())
var probe struct {
ID uint64 `json:"id"`
Method string `json:"method"`
}
if json.Unmarshal(line, &probe) != nil {
continue
}
if probe.Method == "" {
var resp struct {
ID uint64 `json:"id"`
Result json.RawMessage `json:"result"`
}
json.Unmarshal(line, &resp)
pendMu.Lock()
ch, ok := pending[resp.ID]
delete(pending, resp.ID)
pendMu.Unlock()
if ok {
ch <- resp.Result
}
continue
}
var req request
json.Unmarshal(line, &req)
switch req.Method {
case "handshake":
var hp struct {
ShmSize int `json:"shm_size"`
}
json.Unmarshal(req.Params, &hp)
if hp.ShmSize > 0 {
m, err := syscall.Mmap(3, 0, hp.ShmSize,
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
if err != nil {
send(response{ID: req.ID, Error: fmt.Sprintf("mmap: %v", err)})
continue
}
// 统一区域:前 64B 是 SuperBlockStageContext 段在其后
ctxOff := binary.LittleEndian.Uint32(m[20:])
ctxSize := binary.LittleEndian.Uint32(m[24:])
shm = m[ctxOff : ctxOff+ctxSize]
}
send(response{ID: req.ID, Result: map[string]interface{}{
"protocol": 2, "sdk_version": "test",
"plugin_name": "append-" + tag, "pid": os.Getpid(),
}})
case "plugin.init":
send(response{ID: req.ID})
case "plugin.start":
go func(id uint64) {
callKernel("stage.register", map[string]interface{}{
"stage": "after_toolcall",
"scope": "global",
})
send(response{ID: id})
}(req.ID)
case "plugin.stop":
send(response{ID: req.ID})
out.Flush()
os.Exit(0)
case "stage.invoke":
go func(id uint64) {
if shm == nil {
send(response{ID: id, Error: "共享段未挂载"})
return
}
// 拿锁 → 读 → 追加自己的标记 → 写回 → 放锁
callKernel("stage.lock", nil)
cur := readFinalText()
err := writeFinalText(cur + tag)
callKernel("stage.unlock", nil)
if err != nil {
send(response{ID: id, Error: err.Error()})
return
}
send(response{ID: id, Result: map[string]interface{}{"dirty_fields": 1}})
}(req.ID)
default:
if req.ID != 0 {
send(response{ID: req.ID})
}
}
}
}