mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
Part 1 加载分派骨架(迁移可逐插件推进、随时回退的前提):
- dynamic.go: 新增 binEntry/skillEntry 常量 + entryKind 枚举 + classifyEntry/detectEntryKind
manifest entry 优先级最高(改回 plugin.so 即回退 cabi);无 manifest 时按目录探测,.bin 优先
- registry.go: tryDynamic 按 entry 分派 proc/cabi 双通道;
entry 声明 .bin 但二进制缺失时报明确错误,不静默回退(否则'已迁移插件跑回旧通道'极难排查)
- registry.go: pluginEntryHash 候选顺序与 detectEntryKind 对齐(.bin 优先),
否则增量重载会用错文件算 hash
- dynamic_proc_{unix,windows}.go: tryLoadProc 桩位(权限/类型校验已实现,进程管理属 Part 2)
Part 4 共享内存数据面(迁移评估 §3.3/§3.4/§3.7,最关键一环):
- proc/shm.go: 段布局(Header + ShmStageCtx 描述符数组 + append-only arena)
相对偏移设计——各进程 mmap 到不同虚拟地址仍能正确解引用
arena 用尽显式报错而非静默截断(§4.4 风险登记);Compact() 回收 append-only 垃圾
- proc/shmcodec.go: StageContext 16 字段跨进程编解码
字段级描述符消除 lost update:只改 FinalText 的插件不触碰 ToolResults 描述符
WriteDirty 只写脏字段——只读插件零写入,不可能覆盖他人改写
Snapshot 存序列化字符串(切片共享底层数组的坑,C ABI 侧修 11.3 时已踩过)
Extra 4 键提升为具名字段;Response 用标志位表达 nil vs 空串
- proc/lock.go: 锁仲裁回归内核(§3.7 已裁定,零 cgo)
ForceRelease 实现实验 9 的崩溃自愈——排除 robust pthread_mutex 必要性
重复加锁显式拒绝(否则死锁 30s);等待超时有补偿 goroutine 防锁泄漏
验证:
- proc 包 16 项测试全绿(含 -race):全字段往返/只读零写回/原地改切片识别/
现网 sanitizer+weather 场景/5插件×40轮并发零丢失/arena 耗尽报错/压实不破坏字段/
锁互斥·串扰拒绝·崩溃自愈·临界区串行化
- entry 分派 9 项测试全绿;go build ./... exit 0;接口冻结 git diff sdk/ 为空
515 lines
14 KiB
Go
515 lines
14 KiB
Go
package proc
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
|
||
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||
)
|
||
|
||
// StageContext 的跨进程编解码(§3.3 数据面 / §3.4 SDK 封装全部复杂度)。
|
||
//
|
||
// 设计要点:
|
||
//
|
||
// 1. **插件作者永远不接触 Slice{off,len}**。插件进程内保留原生
|
||
// *pubsdk.StageContext,handler 照常读写字段;stage 入口从共享段
|
||
// 反序列化成本地对象,handler 返回时把**脏字段**写回共享段。
|
||
//
|
||
// 2. **字段级描述符消除 lost update**。只改 FinalText 的插件不触碰
|
||
// ToolResults 的描述符,故不存在"只读插件把旧快照写回覆盖他人改写"
|
||
// (今日副本模型实测 35.8~36.8% 丢失,§8.4)。
|
||
//
|
||
// 3. **全部 16 个字段可见**。今日经 C ABI 只下发 10 个字段,外部插件永远
|
||
// 看不到 ContextMsgs/ReasoningContent/TokenUsage/Memory/Extra/Errors(§8.3);
|
||
// 共享内存下全部可见可改——接口形式不变,能力变强。
|
||
//
|
||
// 4. **Extra 的 4 个键提升为具名字段**(§3.3 已核实全部使用点仅这 4 个):
|
||
// media_blocks / media_type / input_source / output_channel。
|
||
// 它们都是内核写、插件读,无并发改写需求;真正需要多插件并发改的
|
||
// (LLMText/FinalText/ToolCalls/Errors)全是强类型字段。
|
||
|
||
// extra 中被提升为具名字段的键。
|
||
const (
|
||
ExtraKeyMediaBlocks = "media_blocks"
|
||
ExtraKeyMediaType = "media_type"
|
||
ExtraKeyInputSource = "input_source"
|
||
ExtraKeyOutputChannel = "output_channel"
|
||
)
|
||
|
||
// WriteAll 把整个 StageContext 写入共享段(内核侧在 stage 开始前调用一次)。
|
||
// 调用方须持有写锁。
|
||
func (s *Segment) WriteAll(sc *pubsdk.StageContext) error {
|
||
sc.RLock()
|
||
snap := captureLocal(sc)
|
||
sc.RUnlock()
|
||
return s.writeLocal(snap)
|
||
}
|
||
|
||
// localCtx 是 StageContext 的值快照,用于在不持有 sc 锁的情况下做编解码。
|
||
type localCtx struct {
|
||
RawMessage string
|
||
UserID string
|
||
GroupID string
|
||
LLMText string
|
||
ReasoningContent string
|
||
FinalText string
|
||
Response *string
|
||
Phase string
|
||
NoMemory bool
|
||
ContextMsgs []map[string]interface{}
|
||
ToolCalls []pubsdk.ToolCall
|
||
ToolResults []pubsdk.ToolResult
|
||
Memory []pubsdk.MemItem
|
||
TokenUsage map[string]int
|
||
Errors []string
|
||
ExtraMediaBlocks interface{}
|
||
ExtraMediaType interface{}
|
||
ExtraInputSource interface{}
|
||
ExtraOutputChan interface{}
|
||
}
|
||
|
||
func captureLocal(sc *pubsdk.StageContext) *localCtx {
|
||
l := &localCtx{
|
||
RawMessage: sc.RawMessage,
|
||
UserID: sc.UserID,
|
||
GroupID: sc.GroupID,
|
||
LLMText: sc.LLMText,
|
||
ReasoningContent: sc.ReasoningContent,
|
||
FinalText: sc.FinalText,
|
||
Response: sc.Response,
|
||
Phase: string(sc.Phase),
|
||
NoMemory: sc.NoMemory,
|
||
ContextMsgs: sc.ContextMsgs,
|
||
ToolCalls: sc.ToolCalls,
|
||
ToolResults: sc.ToolResults,
|
||
Memory: sc.Memory,
|
||
TokenUsage: sc.TokenUsage,
|
||
Errors: sc.Errors,
|
||
}
|
||
if sc.Extra != nil {
|
||
l.ExtraMediaBlocks = sc.Extra[ExtraKeyMediaBlocks]
|
||
l.ExtraMediaType = sc.Extra[ExtraKeyMediaType]
|
||
l.ExtraInputSource = sc.Extra[ExtraKeyInputSource]
|
||
l.ExtraOutputChan = sc.Extra[ExtraKeyOutputChannel]
|
||
}
|
||
return l
|
||
}
|
||
|
||
func (s *Segment) writeLocal(l *localCtx) error {
|
||
putStr := func(f stageField, v string) error {
|
||
sl, err := s.write([]byte(v))
|
||
if err != nil {
|
||
return fmt.Errorf("写入字段 %s: %w", f, err)
|
||
}
|
||
s.setDesc(f, sl)
|
||
return nil
|
||
}
|
||
putJSON := func(f stageField, v interface{}) error {
|
||
if v == nil {
|
||
s.setDesc(f, Slice{})
|
||
return nil
|
||
}
|
||
b, err := json.Marshal(v)
|
||
if err != nil {
|
||
return fmt.Errorf("序列化字段 %s: %w", f, err)
|
||
}
|
||
sl, err := s.write(b)
|
||
if err != nil {
|
||
return fmt.Errorf("写入字段 %s: %w", f, err)
|
||
}
|
||
s.setDesc(f, sl)
|
||
return nil
|
||
}
|
||
|
||
for _, step := range []struct {
|
||
f stageField
|
||
v string
|
||
}{
|
||
{fRawMessage, l.RawMessage},
|
||
{fUserID, l.UserID},
|
||
{fGroupID, l.GroupID},
|
||
{fLLMText, l.LLMText},
|
||
{fReasoningContent, l.ReasoningContent},
|
||
{fFinalText, l.FinalText},
|
||
{fPhase, l.Phase},
|
||
} {
|
||
if err := putStr(step.f, step.v); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
// Response 是 *string:用标志位表达 nil,避免 "" 与 nil 混淆
|
||
if l.Response != nil {
|
||
if err := putStr(fResponse, *l.Response); err != nil {
|
||
return err
|
||
}
|
||
s.setFlag(flagResponseSet, true)
|
||
} else {
|
||
s.setDesc(fResponse, Slice{})
|
||
s.setFlag(flagResponseSet, false)
|
||
}
|
||
s.setFlag(flagNoMemory, l.NoMemory)
|
||
|
||
// 切片/映射字段:nil 与空切片都写成"未设置",避免插件收到 [] 后误以为
|
||
// 内核显式清空过(与今日 writable 的 len>0 才下发语义一致)。
|
||
jsonFields := []struct {
|
||
f stageField
|
||
v interface{}
|
||
}{
|
||
{fContextMsgs, sliceOrNil(len(l.ContextMsgs), l.ContextMsgs)},
|
||
{fToolCalls, sliceOrNil(len(l.ToolCalls), l.ToolCalls)},
|
||
{fToolResults, sliceOrNil(len(l.ToolResults), l.ToolResults)},
|
||
{fMemory, sliceOrNil(len(l.Memory), l.Memory)},
|
||
{fTokenUsage, sliceOrNil(len(l.TokenUsage), l.TokenUsage)},
|
||
{fErrors, sliceOrNil(len(l.Errors), l.Errors)},
|
||
{fExtraMediaBlocks, l.ExtraMediaBlocks},
|
||
{fExtraMediaType, l.ExtraMediaType},
|
||
{fExtraInputSource, l.ExtraInputSource},
|
||
{fExtraOutputChannel, l.ExtraOutputChan},
|
||
}
|
||
for _, step := range jsonFields {
|
||
if err := putJSON(step.f, step.v); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
s.bumpSeq()
|
||
return nil
|
||
}
|
||
|
||
// sliceOrNil 让长度为 0 的容器写成 nil(未设置),非零则原样返回。
|
||
func sliceOrNil(n int, v interface{}) interface{} {
|
||
if n == 0 {
|
||
return nil
|
||
}
|
||
return v
|
||
}
|
||
|
||
// ReadInto 从共享段读出全部字段填充到 sc(插件进程侧 stage 入口调用)。
|
||
// 调用方须持有读锁或写锁。
|
||
func (s *Segment) ReadInto(sc *pubsdk.StageContext) error {
|
||
getStr := func(f stageField) (string, error) {
|
||
b, err := s.read(s.getDesc(f))
|
||
if err != nil {
|
||
return "", fmt.Errorf("读取字段 %s: %w", f, err)
|
||
}
|
||
return string(b), nil
|
||
}
|
||
getJSON := func(f stageField, out interface{}) error {
|
||
b, err := s.read(s.getDesc(f))
|
||
if err != nil {
|
||
return fmt.Errorf("读取字段 %s: %w", f, err)
|
||
}
|
||
if len(b) == 0 {
|
||
return nil
|
||
}
|
||
if err := json.Unmarshal(b, out); err != nil {
|
||
return fmt.Errorf("反序列化字段 %s: %w", f, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
raw, err := getStr(fRawMessage)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
uid, err := getStr(fUserID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
gid, err := getStr(fGroupID)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
llm, err := getStr(fLLMText)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
reason, err := getStr(fReasoningContent)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
final, err := getStr(fFinalText)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
phase, err := getStr(fPhase)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
var ctxMsgs []map[string]interface{}
|
||
var toolCalls []pubsdk.ToolCall
|
||
var toolResults []pubsdk.ToolResult
|
||
var mem []pubsdk.MemItem
|
||
var usage map[string]int
|
||
var errs []string
|
||
if err := getJSON(fContextMsgs, &ctxMsgs); err != nil {
|
||
return err
|
||
}
|
||
if err := getJSON(fToolCalls, &toolCalls); err != nil {
|
||
return err
|
||
}
|
||
if err := getJSON(fToolResults, &toolResults); err != nil {
|
||
return err
|
||
}
|
||
if err := getJSON(fMemory, &mem); err != nil {
|
||
return err
|
||
}
|
||
if err := getJSON(fTokenUsage, &usage); err != nil {
|
||
return err
|
||
}
|
||
if err := getJSON(fErrors, &errs); err != nil {
|
||
return err
|
||
}
|
||
|
||
extra := map[string]interface{}{}
|
||
for _, pair := range []struct {
|
||
f stageField
|
||
key string
|
||
}{
|
||
{fExtraMediaBlocks, ExtraKeyMediaBlocks},
|
||
{fExtraMediaType, ExtraKeyMediaType},
|
||
{fExtraInputSource, ExtraKeyInputSource},
|
||
{fExtraOutputChannel, ExtraKeyOutputChannel},
|
||
} {
|
||
var v interface{}
|
||
if err := getJSON(pair.f, &v); err != nil {
|
||
return err
|
||
}
|
||
if v != nil {
|
||
extra[pair.key] = v
|
||
}
|
||
}
|
||
|
||
var respPtr *string
|
||
if s.getFlag(flagResponseSet) {
|
||
r, err := getStr(fResponse)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
respPtr = &r
|
||
}
|
||
|
||
sc.Lock()
|
||
defer sc.Unlock()
|
||
sc.RawMessage = raw
|
||
sc.UserID = uid
|
||
sc.GroupID = gid
|
||
sc.LLMText = llm
|
||
sc.ReasoningContent = reason
|
||
sc.FinalText = final
|
||
sc.Phase = pubsdk.Stage(phase)
|
||
sc.NoMemory = s.getFlag(flagNoMemory)
|
||
sc.Response = respPtr
|
||
sc.ContextMsgs = ctxMsgs
|
||
sc.ToolCalls = toolCalls
|
||
sc.ToolResults = toolResults
|
||
sc.Memory = mem
|
||
sc.TokenUsage = usage
|
||
sc.Errors = errs
|
||
if len(extra) > 0 {
|
||
sc.Extra = extra
|
||
} else {
|
||
sc.Extra = nil
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// WriteDirty 只把与 base 快照不同的字段写回共享段(插件进程侧 handler 返回后调用)。
|
||
//
|
||
// **这是消除 lost update 的关键**:只读插件的 dirty 集为空 → 零写入 →
|
||
// 不可能覆盖其他插件的改写。对比今日副本模型无条件回传 10 个字段的行为
|
||
// (§8.4 实测 35.8~36.8% 丢失,现网量级百分之几的脏数据进 LLM)。
|
||
//
|
||
// 返回实际写回的字段数,便于诊断与测试断言。
|
||
func (s *Segment) WriteDirty(sc *pubsdk.StageContext, base *Snapshot) (int, error) {
|
||
sc.RLock()
|
||
cur := captureLocal(sc)
|
||
sc.RUnlock()
|
||
|
||
now := newSnapshotFromLocal(cur)
|
||
changed := 0
|
||
|
||
putStr := func(f stageField, v string) error {
|
||
sl, err := s.write([]byte(v))
|
||
if err != nil {
|
||
return fmt.Errorf("写回字段 %s: %w", f, err)
|
||
}
|
||
s.setDesc(f, sl)
|
||
changed++
|
||
return nil
|
||
}
|
||
putRaw := func(f stageField, raw string) error {
|
||
if raw == "" {
|
||
s.setDesc(f, Slice{})
|
||
changed++
|
||
return nil
|
||
}
|
||
sl, err := s.write([]byte(raw))
|
||
if err != nil {
|
||
return fmt.Errorf("写回字段 %s: %w", f, err)
|
||
}
|
||
s.setDesc(f, sl)
|
||
changed++
|
||
return nil
|
||
}
|
||
|
||
for _, step := range []struct {
|
||
f stageField
|
||
v string
|
||
}{
|
||
{fRawMessage, cur.RawMessage},
|
||
{fUserID, cur.UserID},
|
||
{fGroupID, cur.GroupID},
|
||
{fLLMText, cur.LLMText},
|
||
{fReasoningContent, cur.ReasoningContent},
|
||
{fFinalText, cur.FinalText},
|
||
{fPhase, cur.Phase},
|
||
} {
|
||
if base.strs[step.f] != now.strs[step.f] {
|
||
if err := putStr(step.f, step.v); err != nil {
|
||
return changed, err
|
||
}
|
||
}
|
||
}
|
||
|
||
// JSON 字段:比较序列化结果
|
||
for f := range now.jsons {
|
||
if base.jsons[f] != now.jsons[f] {
|
||
if err := putRaw(f, now.jsons[f]); err != nil {
|
||
return changed, err
|
||
}
|
||
}
|
||
}
|
||
|
||
// Response 的 nil 语义变化也算脏
|
||
if base.responseSet != now.responseSet || base.response != now.response {
|
||
if now.responseSet {
|
||
if err := putStr(fResponse, now.response); err != nil {
|
||
return changed, err
|
||
}
|
||
s.setFlag(flagResponseSet, true)
|
||
} else {
|
||
// 插件把 Response 置回 nil:短路语义不应被撑销,故不清空内核已设的值。
|
||
// 与 C ABI 路径 applyStageResult 的行为保持一致。
|
||
s.setFlag(flagResponseSet, s.getFlag(flagResponseSet))
|
||
}
|
||
}
|
||
if base.noMemory != now.noMemory {
|
||
s.setFlag(flagNoMemory, now.noMemory)
|
||
changed++
|
||
}
|
||
|
||
if changed > 0 {
|
||
s.bumpSeq()
|
||
}
|
||
return changed, nil
|
||
}
|
||
|
||
// Snapshot 是 handler 运行前的字段快照,用于计算脏字段。
|
||
//
|
||
// ❗ 必须存**序列化后的字符串**而非 Go 值:StageContext 的切片字段与
|
||
// 调用方共享底层数组,handler 原地改元素(sc.ToolResults[0].Result = x)
|
||
// 时直接持有的 Go 值快照会跟着变,脏字段计算失效——这个坑在 C ABI 侧
|
||
// 修 11.3 时已经踩过一次(见 SDK 仓 stagediff_test.go 的注释)。
|
||
type Snapshot struct {
|
||
strs map[stageField]string
|
||
jsons map[stageField]string
|
||
response string
|
||
responseSet bool
|
||
noMemory bool
|
||
}
|
||
|
||
// Snapshot 抓取当前 StageContext 的快照(插件进程侧 handler 前调用)。
|
||
func TakeSnapshot(sc *pubsdk.StageContext) *Snapshot {
|
||
sc.RLock()
|
||
l := captureLocal(sc)
|
||
sc.RUnlock()
|
||
return newSnapshotFromLocal(l)
|
||
}
|
||
|
||
func newSnapshotFromLocal(l *localCtx) *Snapshot {
|
||
sn := &Snapshot{
|
||
strs: map[stageField]string{},
|
||
jsons: map[stageField]string{},
|
||
}
|
||
sn.strs[fRawMessage] = l.RawMessage
|
||
sn.strs[fUserID] = l.UserID
|
||
sn.strs[fGroupID] = l.GroupID
|
||
sn.strs[fLLMText] = l.LLMText
|
||
sn.strs[fReasoningContent] = l.ReasoningContent
|
||
sn.strs[fFinalText] = l.FinalText
|
||
sn.strs[fPhase] = l.Phase
|
||
|
||
marshal := func(v interface{}) string {
|
||
if v == nil {
|
||
return ""
|
||
}
|
||
b, err := json.Marshal(v)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
return string(b)
|
||
}
|
||
sn.jsons[fContextMsgs] = marshal(sliceOrNil(len(l.ContextMsgs), l.ContextMsgs))
|
||
sn.jsons[fToolCalls] = marshal(sliceOrNil(len(l.ToolCalls), l.ToolCalls))
|
||
sn.jsons[fToolResults] = marshal(sliceOrNil(len(l.ToolResults), l.ToolResults))
|
||
sn.jsons[fMemory] = marshal(sliceOrNil(len(l.Memory), l.Memory))
|
||
sn.jsons[fTokenUsage] = marshal(sliceOrNil(len(l.TokenUsage), l.TokenUsage))
|
||
sn.jsons[fErrors] = marshal(sliceOrNil(len(l.Errors), l.Errors))
|
||
sn.jsons[fExtraMediaBlocks] = marshal(l.ExtraMediaBlocks)
|
||
sn.jsons[fExtraMediaType] = marshal(l.ExtraMediaType)
|
||
sn.jsons[fExtraInputSource] = marshal(l.ExtraInputSource)
|
||
sn.jsons[fExtraOutputChannel] = marshal(l.ExtraOutputChan)
|
||
|
||
if l.Response != nil {
|
||
sn.response = *l.Response
|
||
sn.responseSet = true
|
||
}
|
||
sn.noMemory = l.NoMemory
|
||
return sn
|
||
}
|
||
|
||
// String 让字段枚举在错误信息里可读。
|
||
func (f stageField) String() string {
|
||
switch f {
|
||
case fRawMessage:
|
||
return "raw_message"
|
||
case fUserID:
|
||
return "user_id"
|
||
case fGroupID:
|
||
return "group_id"
|
||
case fLLMText:
|
||
return "llm_text"
|
||
case fReasoningContent:
|
||
return "reasoning_content"
|
||
case fFinalText:
|
||
return "final_text"
|
||
case fResponse:
|
||
return "response"
|
||
case fPhase:
|
||
return "phase"
|
||
case fContextMsgs:
|
||
return "context_msgs"
|
||
case fToolCalls:
|
||
return "tool_calls"
|
||
case fToolResults:
|
||
return "tool_results"
|
||
case fMemory:
|
||
return "memory"
|
||
case fTokenUsage:
|
||
return "token_usage"
|
||
case fErrors:
|
||
return "errors"
|
||
case fExtraMediaBlocks:
|
||
return "extra." + ExtraKeyMediaBlocks
|
||
case fExtraMediaType:
|
||
return "extra." + ExtraKeyMediaType
|
||
case fExtraInputSource:
|
||
return "extra." + ExtraKeyInputSource
|
||
case fExtraOutputChannel:
|
||
return "extra." + ExtraKeyOutputChannel
|
||
}
|
||
return fmt.Sprintf("field(%d)", int(f))
|
||
}
|