feat(scheduler): M5 饥饿防护(抢占计数提升有效级 + 抢占冷却)

设计依据 docs/zh/input-scheduler-design.md §9、§11.5(G1/G2)。

- Task 增加 PreemptCount / LastPreemptAt;effectiveLevel(t) =
  min(L4, Level + min(PreemptCount, 2)):被抢占越多越“值钱”,
  逐步追上抢占它的流,但封顶 L4 因而抢不过真正的紧急输入
- 选择函数 taskBefore 改用有效级;requestPreempt 与 preemptGrantedFor
  同样以有效级比较
- 抢占冷却 preemptCooldown=2s:刚被抢占的任务期内不再被抢占,
  避免同一任务被反复打断到永不完结
- 新增 scheduler_starvation_test.go 4 项:提升与封顶、冷却期内不得再抢占、
  提升后同级不得抢占而更高可、选择函数确实用有效级
- 验收:agent 全量 + -race;全仓 build/vet 通过
This commit is contained in:
JianFeeeee
2026-09-13 00:38:30 +08:00
parent 7565248f61
commit a971fc8877
5 changed files with 180 additions and 25 deletions

View File

@ -121,13 +121,12 @@ type TaskFrame struct {
// 这些字段让帧覆盖 prepare → step… → finish 全生命周期:挂起发生在 run 段的
// 安全点,恢复后由 finish 段统一提交context.Append + emitResponse +
// emitMemoryCandidate因此挂起不会重复提交。
Evt *agentIO.InputEvent
CleanInput string
IsInterrupt bool
StartedAt time.Time
Terminal taskTerminal
Level Level
PreemptCount int
Evt *agentIO.InputEvent
CleanInput string
IsInterrupt bool
StartedAt time.Time
Terminal taskTerminal
Level Level
// SeedMsgs 非空时stepPrepare 不重建 system prompt / 记忆上下文,
// 而是以它为前缀继续D1=A抢占式中断任务继承被打断任务的**只读前缀**)。
@ -148,7 +147,7 @@ func (a *Agent) runTaskSteps(f *TaskFrame) stepOutcome {
for i := 0; i < maxSteps; i++ {
// 安全点:只在 step 之间检查让位。临界区StepToolExec不在此列
// 因为让位信号由 interruptLoop 置位、而本循环是唯一读帧者。
if !a.inCriticalSection() && a.sched.preemptGrantedFor(f.Level) && a.sched.canSuspend() {
if !a.inCriticalSection() && a.sched.preemptGrantedFor() && a.sched.canSuspend() {
return outcomeSuspended
}
switch a.step(f) {