feat(planner): finding 当场唤醒 + planner N 秒心跳触发(任务参数,默认5min)

触发机制补两条(见 docs/planner-trigger-impl-plan.md):

1) finding 当场唤醒:worker 飞行中 report_finding 落库即唤醒本任务 planner,
   不再等 worker 整趟收工才反应。
   - worker.Execute 加 notify 入参并 SetNotify(补上 worker toolset 之前缺失的接线)
   - addFinding 写 KindFinding 成功后调 notify(只对 finding,不对 fact)
   - engine 两处 worker.Execute 调用传 t.Notify

2) planner N 秒心跳:距上轮规划结束/任务开始满 N 秒且期间无触发 → 触发一轮,
   兜底卡死 + 唤醒去监督飞行中的 worker(steer/kill) + 周期复查。
   - plannerLoop 重构为 runRound 闭包 + 三臂 select(ctx/notify/heartbeat)
   - 心跳在 loop 入口臂 = 从任务 start 计时(覆盖 seed 跳过首轮),每轮结束重臂
   - 新增任务参数 plan_heartbeat_seconds,下限=默认=300(5min),低于自动抬到300
   - DB 迁移:schema.sql 加列 + ALTER ADD COLUMN IF NOT EXISTS 补旧库
   - 全链路接线:db.CreateTask/manager/server HTTP/orchestration spawn_task
   - 前端:api.createTask 传参 + 建任务表单新增"planner 心跳(分钟)"字段
This commit is contained in:
Autumn-27 committed 2026-08-12 11:18:07 -04:00
1 parent 06c1ac748f
commit 47ba58bda6
11 files changed
+168 -82

No files matched your search

+8 -6
View File
@@ -36,9 +36,10 @@ type Task struct {
LLMProfileID *int64 `json:"llm_profile_id,omitempty"` // 指定运行本任务 planner/worker 的 LLM 配置;nil=用全局激活配置
Status string `json:"status"` // persisted lifecycle status (done/failed/timeout 为终态;空/其它则由运行态推导)
// 任务级超时(见 docs/任务级超时与收尾设计.md)。DeadlineAt/FirstRunAt 为 unix 秒,0=未设/未运行。
TimeoutSeconds int `json:"timeout_seconds"`
FirstRunAt int64 `json:"first_run_at,omitempty"`
DeadlineAt int64 `json:"deadline_at,omitempty"`
TimeoutSeconds int `json:"timeout_seconds"`
PlanHeartbeatSeconds int `json:"plan_heartbeat_seconds"` // planner 心跳触发间隔(秒)
FirstRunAt int64 `json:"first_run_at,omitempty"`
DeadlineAt int64 `json:"deadline_at,omitempty"`
Store *pgdb.ExplorationStore `json:"-"`
Guard *guard.Guard `json:"-"`
notify chan struct{}
@@ -459,15 +460,16 @@ func taskFromPG(pt *pgdb.Task, store *pgdb.ExplorationStore, ic *intercept.Inter
Description: pt.Description, Goal: pt.Goal, CreatedAt: pt.CreatedAt.Unix(), Paused: pt.Paused,
CompletedAt: unixOrZero(pt.CompletedAt), Status: pt.Status, ParentRef: pt.ParentRef,
LLMProfileID: pt.LLMProfileID,
TimeoutSeconds: pt.TimeoutSeconds, FirstRunAt: unixOrZero(pt.FirstRunAt), DeadlineAt: unixOrZero(pt.DeadlineAt),
TimeoutSeconds: pt.TimeoutSeconds, PlanHeartbeatSeconds: pt.PlanHeartbeatSeconds,
FirstRunAt: unixOrZero(pt.FirstRunAt), DeadlineAt: unixOrZero(pt.DeadlineAt),
Store: store, Guard: guard.NewWithInterceptor(ic), notify: make(chan struct{}, 1),
}
}
// CreateTask creates a task + its exploration and makes it active.
// timeoutSeconds is the task-level wall-clock budget (0 = 不限时).
func (m *Manager) CreateTask(description, goal string, llmProfileID *int64, timeoutSeconds int) (*Task, error) {
pt, err := m.pg.CreateTask(description, goal, llmProfileID, timeoutSeconds)
func (m *Manager) CreateTask(description, goal string, llmProfileID *int64, timeoutSeconds, planHeartbeatSeconds int) (*Task, error) {
pt, err := m.pg.CreateTask(description, goal, llmProfileID, timeoutSeconds, planHeartbeatSeconds)
if err != nil {
return nil, err
}