mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 18:08:04 +00:00
fix(stop): 配额须计入停在输入 channel 的待处理消息
实测:停止后排队消息仍逐条跑完。原因是 armStop 只数 sched.queue, 而用户按下停止时调度器正忙于当前任务,其余消息大多还没被 pumpInbox 搬进队列、仍停在 inputCh ⇒ queued=0、配额归零。 - IOManager.PendingInputs():暴露 channel 中待处理条数。 - armStop(pending int):queued = len(s.queue) + pending。 - 补 TestStop_ArmCountsPendingChannelInputs 锁死该口径。
This commit is contained in:
@ -53,7 +53,14 @@ func (a *Agent) interceptLoop() {
|
||||
// 这个 `||` 在 queued=0 时会短路到 takeStop(),把标记先消费掉,
|
||||
// 于是 stepLLM 永远看不到它 → 取消后照样重跑一轮。
|
||||
// 实测:停止被正确记录(`stop requested ... queued=0`)但生成仍跑到自然结束。
|
||||
n := a.sched.armStop()
|
||||
// pending 必须算上**停在输入 channel 里**的那一段:停止时
|
||||
// 调度器多在半路忙当前任务,其余消息还没被 pumpInbox 搬进队列,
|
||||
// 只数 sched.queue 会得到 0,配额随之失效(实测过)。
|
||||
pending := 0
|
||||
if a.io != nil {
|
||||
pending = a.io.PendingInputs()
|
||||
}
|
||||
n := a.sched.armStop(pending)
|
||||
log.Printf("[agent] stop requested by %s/%s (queued=%d will be short-circuited at pre-action)",
|
||||
evt.Source, evt.OutputChannel, n)
|
||||
a.cancelCurrentLLM()
|
||||
|
||||
@ -590,21 +590,20 @@ func (s *scheduler) setImmediateLocked(t *Task) bool {
|
||||
// 停止之后新到的输入不受影响(否则停止会变成一个永远生效的“黑洞”)。
|
||||
//
|
||||
// 多次按停止取**较大值**而不是累加:两个客户端同时按下时配额不应翻倍。
|
||||
func (s *scheduler) armStop() int {
|
||||
func (s *scheduler) armStop(pending int) int {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if n := s.pendingQueueLenLocked(); n > s.cancelBudget {
|
||||
s.cancelBudget = n
|
||||
// pending 是**还没被 pumpInbox 搬进队列**的那一段(停在输入 channel 里)。
|
||||
// 用户按下停止时,调度器通常正忙于当前任务,其它消息基本都停在 channel;
|
||||
// 只数 s.queue 会得到 0(实测),配额随之失效。
|
||||
queued := len(s.queue) + pending
|
||||
if queued > s.cancelBudget {
|
||||
s.cancelBudget = queued
|
||||
}
|
||||
s.stopArmed = true
|
||||
return s.cancelBudget
|
||||
}
|
||||
|
||||
// pendingQueueLenLocked 统计**尚未执行**的排队输入数量(不含中断队列/挂起)。
|
||||
func (s *scheduler) pendingQueueLenLocked() int {
|
||||
return len(s.queue)
|
||||
}
|
||||
|
||||
// takeStop 消费「当前任务应被立即结束而不是重试」这一次标记。
|
||||
//
|
||||
// 取消 LLM 后 stepLLM 会看到 context.Canceled 并 outcomeContinue 重跑;
|
||||
|
||||
@ -105,7 +105,7 @@ func TestStop_TerminatesCurrentTaskWithoutRetry(t *testing.T) {
|
||||
a.io.InjectInterrupt(stopEvt.Source, stopEvt.OutputChannel, stopEvt.Payload)
|
||||
// interceptLoop 才是 armStop 的调用点;这里模拟它已消费该指令(只 arm,
|
||||
// 不 takeStop——takeStop 必须由 stepLLM 去消费,那正是被测行为)。
|
||||
a.sched.armStop()
|
||||
a.sched.armStop(0)
|
||||
a.cancelCurrentLLM()
|
||||
|
||||
select {
|
||||
@ -135,7 +135,7 @@ func TestStop_CancelBudgetIsSnapshot(t *testing.T) {
|
||||
// 停止那一刻队列里有 2 条。
|
||||
a.sched.enqueue(newInputTask(mustTextEvent("qq", "一")))
|
||||
a.sched.enqueue(newInputTask(mustTextEvent("qq", "二")))
|
||||
n := a.sched.armStop()
|
||||
n := a.sched.armStop(0)
|
||||
if n != 2 {
|
||||
t.Fatalf("停止时排队深度=%d,期望 2", n)
|
||||
}
|
||||
@ -170,10 +170,10 @@ func TestStop_ArmTwiceTakesMaxNotSum(t *testing.T) {
|
||||
a := newPreemptAgent(t, newPreemptProvider())
|
||||
a.sched.enqueue(newInputTask(mustTextEvent("qq", "一")))
|
||||
a.sched.enqueue(newInputTask(mustTextEvent("qq", "二")))
|
||||
if n := a.sched.armStop(); n != 2 {
|
||||
if n := a.sched.armStop(0); n != 2 {
|
||||
t.Fatalf("首次 armStop=%d,期望 2", n)
|
||||
}
|
||||
if n := a.sched.armStop(); n != 2 {
|
||||
if n := a.sched.armStop(0); n != 2 {
|
||||
t.Fatalf("重复 armStop=%d,期望仍为 2(取 max 不累加)", n)
|
||||
}
|
||||
if !a.sched.consumeCancel() || !a.sched.consumeCancel() {
|
||||
@ -189,6 +189,37 @@ func mustTextEvent(source, text string) *agentIO.InputEvent {
|
||||
return evt
|
||||
}
|
||||
|
||||
// 停止时必须把**停在输入 channel 里**的待处理消息也计入配额。
|
||||
//
|
||||
// 这是实测踩到的坑:用户按下停止时调度器正忙于当前任务,其它消息大多还没被
|
||||
// pumpInbox 搬进就绪队列,仍然停在 inputCh。只数 sched.queue 会得到 queued=0,
|
||||
// 配额归零,停止后排队消息照旧逐条跑完。
|
||||
func TestStop_ArmCountsPendingChannelInputs(t *testing.T) {
|
||||
a := newPreemptAgent(t, newPreemptProvider())
|
||||
|
||||
// 两条消息只进 channel,不入队(模拟 pumpInbox 尚未搬运)。
|
||||
for _, txt := range []string{"一", "二"} {
|
||||
evt, _ := textEvent("webui", txt)
|
||||
a.io.InjectInput("webui", "text", map[string]interface{}{"content": txt})
|
||||
_ = evt
|
||||
}
|
||||
pending := a.io.PendingInputs()
|
||||
if pending != 2 {
|
||||
t.Fatalf("channel 待处理=%d,期望 2", pending)
|
||||
}
|
||||
|
||||
n := a.sched.armStop(pending)
|
||||
if n != 2 {
|
||||
t.Fatalf("armStop(2)=%d,期望 2(配额须包含 channel 中的待处理)", n)
|
||||
}
|
||||
if !a.sched.consumeCancel() || !a.sched.consumeCancel() {
|
||||
t.Fatal("两条配额都应可消费")
|
||||
}
|
||||
if a.sched.consumeCancel() {
|
||||
t.Fatal("不应有多余配额")
|
||||
}
|
||||
}
|
||||
|
||||
// 停止必须经**真实的 interceptLoop** 被消费,而不是测试自己 arm。
|
||||
//
|
||||
// 这条用例专门镇一个已经发生过的自伤:曾经在 interceptLoop 里写成
|
||||
|
||||
@ -536,6 +536,19 @@ func (m *IOManager) EmitTextTo(target, outputChannel, text string) {
|
||||
func (m *IOManager) InputChan() <-chan *InputEvent { return m.inputCh }
|
||||
func (m *IOManager) OutputChan() <-chan *OutputEvent { return m.outputCh }
|
||||
|
||||
// PendingInputs 返回**尚未被调度器取走**的输入条数(排队队列之外的那一段)。
|
||||
//
|
||||
// 为何需要单独一个口径:输入先进 inputCh,再由 pumpInbox 搬进调度器就绪队列。
|
||||
// “停止”要统计的是“用户按下停止时还没被处理的消息”,而它们大多还躺在
|
||||
// inputCh 里(调度器正忙于处理当前任务)。只数 sched.queue 会得到 0,
|
||||
// 于是短路配额失效——实测就是这个现象:停止后排队消息照旧逐条跑完。
|
||||
func (m *IOManager) PendingInputs() int {
|
||||
if m == nil {
|
||||
return 0
|
||||
}
|
||||
return len(m.inputCh)
|
||||
}
|
||||
|
||||
// RegisterInputChannel 注册一个 inputch(不带插件归属,兼容旧调用)。
|
||||
//
|
||||
// inputch 是**最基本的输入路由单位**;一个插件可以注册多个。
|
||||
|
||||
Reference in New Issue
Block a user