mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 14:57:38 +00:00
## 死锁(这是 CI 连续两次红的真因) `StopAll` 与 `StopPlugin` 都在持有 `r.mu` **写锁**时调用 `RemoveBuiltinSkill` → `skillDirOf` → `r.mu.RLock()` ⇒ **自死锁**。 后果不是报错而是**卡死**: - internal/plugins 卡 300s 超时(TestCapability_RestrictedPluginStillLoads) - internal/plugin 卡 840s 超时(TestStopPluginDoesNotTouchPersistentState) - CI 的 `go test ./...` 与 `go test -race` 双双红 修法:拆出**不加锁**的内部版本,持锁路径只走它: - `skillDirFrom(dataDir, name)`(纯函数) - `removeBuiltinSkillLocked(pluginName, p, dataDir)`(显式传 dataDir, 避免"看起来没取锁、一改就取锁"的隐患) - `announceSkillRemovedLocked`(持锁下直读 r.evBus) 修后:internal/plugins 300s → 12s,internal/plugin 840s → 0.7s。 ## 防回归判据 `TestStopAllAndStopPluginDoNotDeadlock`:死锁**不返回**,普通断言抓不到, 所以用**超时**当判据(5 秒未返回即失败),并指明应改用 Locked 版本。 ## tools_ready 事件声明 新增的 tools_ready 触发 `TestEveryPublishedEventHasAnOutwardPath` (该判据防"事件发了、客户端收不到"的静默失效)。 tools_ready 是**内核内部**就绪信号(plugin-host → seq 等订阅者), 不是对话流程事件,前端无需感知 ⇒ 在 allowed 里显式声明理由。 ## 验证 go build ./... / go vet ./... / go test -p 1 ./... / -race 全通过。
91 lines
3.3 KiB
Go
91 lines
3.3 KiB
Go
package seq
|
||
|
||
// 开机序列:系统就绪时自动执行标记了 boot 的序列。
|
||
//
|
||
// ## 为什么这是 seq 最核心的用途(用户原话)
|
||
//
|
||
// 「seq 最核心的是定时复用,比如开机执行一段序列」
|
||
//
|
||
// 单次任务里编序列往往**不划算**:先 seq_create 写 JSON 再 seq_run,
|
||
// 比直接调工具多两步(e2e 实测:多文件读取场景模型直接并行 files_read
|
||
// 就是最优解,包成序列纯亏)。真正省事的是**同一套流程反复跑** ——
|
||
// 而开机是最典型的周期性触发点。
|
||
//
|
||
// ## 时序(为什么订阅 tools_ready 而不是在 Start 里跑)
|
||
//
|
||
// 插件 Start 的先后由依赖图决定,此刻:
|
||
// · 序列要调的工具可能属于还没拉起的插件
|
||
// · agent 循环还没开始,投递的输入无人接收
|
||
//
|
||
// plugin-host 在 `ks.PluginsReady = true`(全部插件含内置都已加载)
|
||
// 之后发布 tools_ready,那一刻**工具才全部可调**。
|
||
// 本文件只订阅那一个事件 —— 不需要等 agent 起来。
|
||
|
||
import (
|
||
"log"
|
||
|
||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||
)
|
||
|
||
// subscribeBoot 订阅 tools_ready,在该时刻执行所有 boot 序列。
|
||
//
|
||
// 订阅 tools_ready 而不是"agent 就绪":seq 自动运行**不依赖 agent** ——
|
||
// 它只是一段工具调用序列,返回值进队列等 agent 起来后处理即可。
|
||
// 绑在 agent 启动上会把时机人为推后,且依赖编排层的判断。
|
||
//
|
||
// 返回退订函数(Stop 时调用)。订阅失败不阻断插件启动:
|
||
// 工具本身仍可用,只是开机序列不会跑 —— 但必须留日志,
|
||
// 否则"为什么开机没执行"完全无从查起。
|
||
func (p *Plugin) subscribeBoot() func() {
|
||
if p.sdk == nil {
|
||
return func() {}
|
||
}
|
||
unsub := p.sdk.Subscribe(events.EventToolsReady, func(evt *events.Event) {
|
||
// 事件在总线 goroutine 上分发,不能阻塞它:开机序列可能跑很久。
|
||
go p.runBootSequences()
|
||
})
|
||
return unsub
|
||
}
|
||
|
||
// runBootSequences 依次执行所有 boot 序列。
|
||
//
|
||
// 串行执行(不是并发):开机序列通常彼此独立,但并发跑会让日志交错、
|
||
// 且若两条序列争用同一资源,失败原因极难定位。开机不是性能敏感路径。
|
||
//
|
||
// 单条失败不影响后续序列 —— 一条坏了不该让其余开机动作全部失效。
|
||
func (p *Plugin) runBootSequences() {
|
||
names := p.store.List()
|
||
var boots []string
|
||
for _, n := range names {
|
||
sq, err := p.store.Load(n)
|
||
if err != nil {
|
||
// 坏序列不阻断:store 里可能有历史遗留文件
|
||
log.Printf("[seq] boot scan skip %q: %v", n, err)
|
||
continue
|
||
}
|
||
if sq.Boot {
|
||
boots = append(boots, n)
|
||
}
|
||
}
|
||
if len(boots) == 0 {
|
||
return
|
||
}
|
||
|
||
log.Printf("[seq] tools_ready: running %d boot sequence(s): %v", len(boots), boots)
|
||
for _, name := range boots {
|
||
sq, err := p.store.Load(name)
|
||
if err != nil {
|
||
log.Printf("[seq] boot %q load failed: %v", name, err)
|
||
continue
|
||
}
|
||
// 开机序列无外部入参(nil):组的 when/args 按既有语义取零值。
|
||
res, err := p.runSequence(sq, nil, nil)
|
||
if err != nil {
|
||
log.Printf("[seq] boot %q failed: %v", name, err)
|
||
continue
|
||
}
|
||
// runSequence 返回渲染后的摘要(interface{}),含失败标记时如实转述。
|
||
log.Printf("[seq] boot %q done (%d groups): %v", name, len(sq.Groups), res)
|
||
}
|
||
}
|