From 4835cb57bd093fcecc9425c203e33ad628c2fe82 Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Wed, 12 Aug 2026 11:37:54 -0400 Subject: [PATCH] =?UTF-8?q?fix(engine):=20=E7=A7=8D=E5=AD=90=E6=84=8F?= =?UTF-8?q?=E5=9B=BE=E4=BB=BB=E5=8A=A1=E8=AF=AF=E8=A7=A6=E5=8F=91=E9=A6=96?= =?UTF-8?q?=E8=BD=AE=20planner(Frontier=20=E5=8F=AA=E6=95=B0=20open=20?= =?UTF-8?q?=E4=B8=8E=20worker=20claim=20=E7=AB=9E=E6=80=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 里「跳过首轮」依赖种子意图让 frontier 非空,但 Frontier 只数 state='open', 而 Run 先起 worker、后查 frontier——worker 抢先 claim 种子意图(open→running)后, Frontier(open) 变空 → 误发 t.Notify() → 首轮 planner 立刻触发(日志 "edge 触发")。 改为 HasActiveIntent(open 或 running):种子无论 open 还是已被 claim 成 running 都算 「有活干」,一律跳过首轮。首轮延后到种子 worker 收工(NotifyDone)/出 finding/心跳。 顺带修好重启恢复时只剩 running 意图也会误 kick 的情况。 --- db/exploration.go | 12 ++++++++++++ server/engine.go | 10 ++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/db/exploration.go b/db/exploration.go index 8c66cdc..ee8c902 100644 --- a/db/exploration.go +++ b/db/exploration.go @@ -381,6 +381,18 @@ ORDER BY priority DESC, id ASC LIMIT $2`, s.expID, limit) return scanNodes(rows) } +// HasActiveIntent reports whether this exploration has any intent still in play +// (state open OR running). Used to decide whether to kick the FIRST planner round: +// a seeded task's intent may already have been claimed (open→running) by a worker +// before the check, so counting only 'open' (Frontier) races with worker claim and +// wrongly kicks a redundant first round. Counting open+running is race-proof. +func (s *ExplorationStore) HasActiveIntent() (bool, error) { + var exists bool + err := s.db.QueryRow(`SELECT EXISTS(SELECT 1 FROM exploration_nodes +WHERE exploration_id=$1 AND kind='intent' AND state IN ('open','running'))`, s.expID).Scan(&exists) + return exists, err +} + // ClaimIntent atomically moves an open intent to running. Returns true if claimed. func (s *ExplorationStore) ClaimIntent(id int64, owner string) (bool, error) { res, err := s.db.Exec(`UPDATE exploration_nodes SET state='running', owner=$1 diff --git a/server/engine.go b/server/engine.go index 3c5a5fa..c64c5d3 100644 --- a/server/engine.go +++ b/server/engine.go @@ -433,10 +433,12 @@ func (e *Engine) Run(ctx context.Context, t *Task) { go e.workerLoop(ctx, t, fmt.Sprintf("work#%d", i+1)) } e.startDeadlineCoordinator(ctx, t) // 任务级超时定时器(仅 timeout>0;去重) - // 仅在 frontier 为空时才 kick 首轮规划:普通任务(空 frontier)照常触发;带种子意图的 - // 任务 frontier 已非空 → 跳过首轮 planner,worker 直接领种子意图开跑,跑完由 NotifyDone - // 唤醒 planner。重启自动恢复时 frontier 通常也非空 → 顺带避免白烧一轮。 - if fr, _ := t.Store.Frontier(1); len(fr) == 0 { + // 仅在「完全没有活动意图(open+running)」时才 kick 首轮规划。带种子意图的任务:种子已 + // 是 open,或已被上面刚起的 worker 抢先 claim 成 running——两种都算「有活干」,一律跳过 + // 首轮 planner,worker 直接领种子意图开跑,跑完由 NotifyDone/心跳唤醒 planner。 + // ⚠️ 不能用 Frontier(只数 open):worker 领取(open→running)与本检查存在竞态,会误 kick。 + // 重启自动恢复时也可能只剩 running 意图,同样应跳过。 + if has, _ := t.Store.HasActiveIntent(); !has { t.Notify() // kick the first planning round (acted on once LLM is ready) } }