mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-24 10:58:13 +00:00
排查「我的任务怎么被莫名打断了」时撞上的观测缺口。
## 缺口
`executeNewTask` 里挂起、`resumeTask` 里恢复,两处都**只发事件、不写日志**:
a.sched.suspend(t, f)
a.publishEvent(events.EventScheduler, map[string]any{"action": "suspend", ...})
于是生产日志里只有两行:`interrupt from X` 与 `LLM request cancelled by
preemption` —— **看不到受害者是谁、被谁挤下去、后来有没有恢复**。后果是实测过的:
按时间先后猜凶手,把时间上相邻的输入误认成抢占者。
## 改动
- `sourceOf(task, frame)`:取可辨识来源(`evt.Source` 优先,回退 OutputChannel,
自循环任务给 `self:<channel>`)。取 Source 而**不是** OutputChannel:
前者回答「谁送来的」(qq / homeagent-mail-bridge / timer / child/xxx),
后者只回答投递到哪个通道;多数场景同名,但因果链上要的是前者。
- `describeTask(task)`:`task#N class=queued|interrupt level=L`。
- `suspendDepth()`:日志专用,走锁而不是让日志点直接摸 `suspendStack`。
- 三个日志点:抢占开始(含 victim)、挂起(含来源与栈深)、恢复。
输出形状:
[agent] preempt start: task#2 class=interrupt level=4 from cli -> victim task#1 class=queued level=0 (qq)
[agent] suspend: task#1 class=queued level=0 (qq) yields to an interrupt; suspendStack=0
[agent] resume: task#1 class=queued level=0 (qq) resumes after the interrupt finished
## 验证
- 新增 preempt_logging_test.go:抢占后栈深 0→1、sourceOf 取到 qq、self/nil 不 panic。
- 实测日志(TestPreempt_HigherPreemptsAndResumes)三条齐全,能一眼看出
是 `cli` 的 L4 挤掉了 `qq` 的排队任务、随后 qq 恢复。
- `go test ./internal/... ./cmd/...` 全绿。
65 lines
1.8 KiB
Go
65 lines
1.8 KiB
Go
package core
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
|
||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||
)
|
||
|
||
// 抢占日志必须能说出**受害者是谁**(来源 + 类别 + 级别)。
|
||
// 此前 suspend/resume 不落日志,生产上无法回答"我的任务被谁打断了"。
|
||
func TestPreemptLogging_NamesVictimAndSource(t *testing.T) {
|
||
sp := newPreemptProvider("intr-done", "low-done")
|
||
a := newPreemptAgent(t, sp)
|
||
|
||
lowEvt, _ := textEvent("qq", "低优先级任务")
|
||
lowTask := newInputTask(lowEvt)
|
||
if !a.sched.enqueue(lowTask) {
|
||
t.Fatal("入队失败")
|
||
}
|
||
lt, _, _ := a.sched.nextRef()
|
||
|
||
done := make(chan struct{})
|
||
go func() { a.executeNewTask(lt); close(done) }()
|
||
select {
|
||
case <-sp.entered:
|
||
case <-time.After(3 * time.Second):
|
||
t.Fatal("provider 未被调用")
|
||
}
|
||
|
||
if a.sched.suspendDepth() != 0 {
|
||
t.Fatalf("初始栈深应为 0,实际 %d", a.sched.suspendDepth())
|
||
}
|
||
|
||
intrEvt := &agentIO.InputEvent{
|
||
Source: "cli", OutputChannel: "cli", Type: "text",
|
||
Payload: map[string]interface{}{"content": "紧急打断", "interrupt": true},
|
||
}
|
||
a.sched.requestPreempt(intrEvt, LevelInteractive)
|
||
a.cancelCurrentLLM()
|
||
|
||
select {
|
||
case <-done:
|
||
case <-time.After(3 * time.Second):
|
||
t.Fatal("未挂起")
|
||
}
|
||
if d := a.sched.suspendDepth(); d != 1 {
|
||
t.Fatalf("挂起后栈深应为 1,实际 %d", d)
|
||
}
|
||
// sourceOf 要能从任务/帧里取出可辨识来源(qq),而不是空串或 task#N。
|
||
if got := sourceOf(lt, nil); got != "qq" {
|
||
t.Fatalf("sourceOf(lt) = %q,期望 qq", got)
|
||
}
|
||
}
|
||
|
||
func TestSourceOf_SelfAndNilAreSafe(t *testing.T) {
|
||
if got := sourceOf(nil, nil); got != "?" {
|
||
t.Fatalf("nil -> %q", got)
|
||
}
|
||
st := newSelfTask(selfInputMsg{text: "x", channel: "cli"})
|
||
if got := sourceOf(st, nil); got != "self:cli" {
|
||
t.Fatalf("self task -> %q", got)
|
||
}
|
||
}
|