Files
HomeAgent/internal/agent/core/scheduler_starvation_test.go
JianFeeeee d4764e3682 fix(scheduler)!: D1 更正为「中断从上一个任务之前的完整状态开始」,并实现现场合回
用户明确语义(我此前对 D1 的解析就是错的——当时回答里的“A”指的是 git 选项,
D1 实际要的是方案 B):

  中断打断时,上个任务到达以来的所有上下文现场被保护(含 toolcall),
  然后中断在「上个任务前的那个完整状态」上开始运行;
  中断结束后再把被挂起的任务与其上下文现场加载回中断任务之上,并继续运行。

实现:
- 删除 SeedMsgs 与 D1=A 的“只读前缀”机制:中断任务不再继承被打断任务的任何内容,
  它就是普通新任务,正常走完整 prepare(system prompt + timeline + 自己的输入)
- TaskFrame 新增 PrefixLen(基础前缀长度)与 InputBlocks;
  stepPrepare 在 buildMessages 之后记录 PrefixLen
- 新增 rebaseFramePrefix:恢复时重建基础前缀(中断已提交进 a.context,
  重建的 timeline 含中断效果=“加载回中断之上”),再把本任务自己的尾部
  (Stage 上下文 + 工具轮产物 + 占位)接回;并补回 IsInterrupt 标记与多模态块
- resumeTask 在 runTaskSteps 之前调用 rebaseFramePrefix
- 设计稿 §5.3 改写为「已定:D1=B」并写明实现对应;§6.2 补“重建前缀→接回尾部”;
  §12 的 D1 行更新

测试:
- TestPreempt_HigherPreemptsAndResumes 改为断言「中断不继承、恢复后看得见中断内容」
- 新增 TestPreempt_ResumeRebaseRestoresTailDecorations(前缀重建后尾部装饰补回)
- 原 TestPreempt_SeedPathDoesNotLeakInterruptFlag 随之删除(机制已不存在)

验收:agent 全量 + -race;全仓 build/vet 通过
2026-09-13 06:24:20 +08:00

119 lines
4.1 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.

package core
// M5 验收测试:饥饿防护(抢占计数提升有效级 + 抢占冷却)。
//
// 设计依据 docs/zh/input-scheduler-design.md §9、§11.5G1/G2
//
// 为什么需要:固定四级 + 「严格大于才抢占」下,一条 L4 流可以反复打断同一个
// L1 任务,使它永不完结。提升被抢占者的**有效**优先级,让它在竞争排队时
// 逐步追上;封顶 L4因此它永远抢不过真正的紧急输入紧急输入本身不被抢占
import (
"testing"
"time"
)
func TestStarvation_EffectiveLevelPromotion(t *testing.T) {
base := &Task{Level: LevelBackground}
if got := effectiveLevel(base); got != LevelBackground {
t.Fatalf("未抢占时有效级=%v期望 L1", got)
}
base.PreemptCount = 1
if got := effectiveLevel(base); got != LevelMessage {
t.Fatalf("被抢占 1 次后有效级=%v期望 L2", got)
}
base.PreemptCount = 2
if got := effectiveLevel(base); got != LevelInteractive {
t.Fatalf("被抢占 2 次后有效级=%v期望 L3", got)
}
base.PreemptCount = 99
if got := effectiveLevel(base); got != LevelInteractive {
t.Fatalf("提升应封顶在 +2 档,实际 %v", got)
}
// 封顶 L4L3 任务被多次抢占也不会超过紧急级。
high := &Task{Level: LevelInteractive, PreemptCount: 99}
if got := effectiveLevel(high); got != LevelCritical {
t.Fatalf("L3 提升后应封顶为 L4实际 %v", got)
}
}
func TestStarvation_CooldownBlocksImmediateRepreempt(t *testing.T) {
a := newPreemptAgent(t, newPreemptProvider())
low := &Task{ID: 1, Level: LevelBackground, EnqueuedAt: time.Now()}
a.sched.enqueue(low)
a.sched.nextRef() // running = low
e1, _ := textEvent("qq", "第一次打断")
if !a.sched.requestPreempt(e1, LevelMessage) {
t.Fatal("L2 应能抢占 L1首次")
}
a.sched.suspend(low, a.newTaskFrame("x", a.stageCtxFromInput("x", "", "")))
if low.PreemptCount != 1 {
t.Fatalf("PreemptCount=%d期望 1", low.PreemptCount)
}
if low.LastPreemptAt.IsZero() {
t.Fatal("挂起必须记录 LastPreemptAt冷却起点")
}
// 冷却期内:即使 L4 也不得再抢占。
a.sched.mu.Lock()
a.sched.running = low
a.sched.mu.Unlock()
e2, _ := textEvent("cli", "冷却期内的紧急打断")
if a.sched.requestPreempt(e2, LevelCritical) {
t.Fatal("抢占冷却期内不得再抢占")
}
if a.sched.preemptGrantedFor() {
t.Fatal("冷却期内不得 arm 让位信号")
}
}
func TestStarvation_PromotionBlocksSameLevelPreempt(t *testing.T) {
a := newPreemptAgent(t, newPreemptProvider())
low := &Task{ID: 1, Level: LevelBackground, EnqueuedAt: time.Now()}
a.sched.enqueue(low)
a.sched.nextRef()
// 模拟「已被抢占过一次」:有效级 = L2。
low.PreemptCount = 1
low.LastPreemptAt = time.Now().Add(-time.Hour) // 冷却已过
e1, _ := textEvent("qq", "同级打断")
if a.sched.requestPreempt(e1, LevelMessage) {
t.Fatal("有效级 L2 时L2 中断不得抢占(严格大于才抢占)")
}
e2, _ := textEvent("cli", "更高级打断")
if !a.sched.requestPreempt(e2, LevelInteractive) {
t.Fatal("L3 应能抢占有效级 L2")
}
if !a.sched.preemptGrantedFor() {
t.Fatal("L3 > 有效 L2应已 arm")
}
}
// 选择函数必须用有效级:被抢占过的任务在排队时应当优先于同级/更低的任务。
func TestStarvation_SelectionUsesEffectiveLevel(t *testing.T) {
base := time.Now()
promoted := &Task{ID: 1, Level: LevelBackground, PreemptCount: 2, EnqueuedAt: base} // 有效 L3
normal := &Task{ID: 2, Level: LevelMessage, EnqueuedAt: base.Add(time.Second)} // L2
if !taskBefore(promoted, normal) {
t.Fatal("被抢占 2 次的 L1有效 L3应先于 L2 执行")
}
if taskBefore(normal, promoted) {
t.Fatal("选择函数不得只看基础级")
}
// 提升不改变调度器自身的排序稳定性:同为有效级时按入队时刻。
a := &Task{ID: 3, Level: LevelBackground, PreemptCount: 1, EnqueuedAt: base.Add(2 * time.Second)} // 有效 L2
b := &Task{ID: 4, Level: LevelMessage, EnqueuedAt: base.Add(time.Second)} // L2更早
if !taskBefore(b, a) {
t.Fatal("同有效级时应先到先服务")
}
}