From f57cf659b5dd250baac98f435d693043ff317a23 Mon Sep 17 00:00:00 2001 From: BaaRai <130491490+BaaRai@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:36:44 +0200 Subject: [PATCH] feat: planner quiet-mode skips edge rounds while work runs and goals hold Every worker write-back fired a full planning round even when the open-goal set had not changed; on a live three-worker task that was 7 rounds in 45 minutes. Edge-triggered rounds are now dropped while active intents exist and the open-goal fingerprint recorded at the end of the last round is unchanged; heartbeat rounds, goal-set changes and frontier drains still plan immediately. Fails open on store errors. --- db/exploration.go | 22 +++++++++++ server/engine.go | 37 ++++++++++++++++++ server/planner_quiet_test.go | 76 ++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 server/planner_quiet_test.go diff --git a/db/exploration.go b/db/exploration.go index 7c54b28..496bc21 100644 --- a/db/exploration.go +++ b/db/exploration.go @@ -1192,6 +1192,28 @@ WHERE exploration_id=$1 AND kind='goal' AND state='open')`, s.expID).Scan(&exist return exists, err } +// OpenGoalFingerprint returns a stable fingerprint of the open-goal set: the +// sorted goal ids joined by commas, "" when none. Planner quiet-mode (#79) uses +// it to tell "the goal set changed since the last planning round" apart from +// "nothing the plan depends on changed". +func (s *ExplorationStore) OpenGoalFingerprint() (string, error) { + rows, err := s.db.Query(`SELECT id FROM exploration_nodes +WHERE exploration_id=$1 AND kind='goal' AND state='open' ORDER BY id`, s.expID) + if err != nil { + return "", err + } + defer rows.Close() + var ids []string + for rows.Next() { + var id int64 + if err := rows.Scan(&id); err != nil { + return "", err + } + ids = append(ids, strconv.FormatInt(id, 10)) + } + return strings.Join(ids, ","), rows.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 a459fb5..347776b 100644 --- a/server/engine.go +++ b/server/engine.go @@ -99,6 +99,10 @@ type Engine struct { started sync.Map // taskID -> bool, so Run is idempotent per task lastAct sync.Map // taskID -> int64 unix, last planner/worker activity (heartbeat) llmCalls sync.Map // taskID -> *int64, actual planner/worker/main-agent LLM calls + // quiet-mode (#79): open-goal fingerprint recorded at the end of the last + // completed planning round, per task. Edge-triggered rounds are skipped + // while it is unchanged and work is still in flight. + goalFP sync.Map // taskID -> string (sorted open-goal ids joined) paused sync.Map // taskID -> bool, user-paused (planner + workers idle but loops alive) deleting sync.Map // taskID -> bool, delete barrier (no new task-owned writes) dropCnt sync.Map // taskID -> *int64, running count of dropped (unpersistable) activity records @@ -797,6 +801,22 @@ func resetPlannerTimer(timer *time.Timer, d time.Duration) { timer.Reset(d) } +// quietSkipEdge reports whether an edge-triggered planning round should be +// dropped (#79): the open-goal set is unchanged since the last completed round +// AND active work exists (open or running intents). Fails open (false) on any +// store error: when in doubt, plan, preserving the pre-quiet-mode behavior. +func (e *Engine) quietSkipEdge(t *Task) bool { + fp, err := t.Store.OpenGoalFingerprint() + if err != nil { + return false + } + if last, ok := e.goalFP.Load(t.ID); !ok || last != fp { + return false + } + active, err := t.Store.HasActiveIntent() + return err == nil && active +} + func (e *Engine) plannerLoop(ctx context.Context, t *Task) { interval := plannerHeartbeatInterval(t) // Arming the heartbeat timer at loop entry = counting from the task start: even a @@ -856,6 +876,17 @@ func (e *Engine) plannerLoop(ctx context.Context, t *Task) { } return // the goalless branch never enters planner.Plan } + // quiet-mode (#79): an edge round is redundant while work is in flight + // and the open-goal set is unchanged since the last completed round — + // there is no new decision for the plan to make. Drain the triggers so + // they do not accumulate and let the heartbeat supervise; the next edge + // after a goal opens or closes, or once the frontier drains and all + // workers are idle, plans for real. + if src == "edge" && e.quietSkipEdge(t) { + t.drainTriggers() + log.Printf("[planner] task %s quiet: work in flight, goals unchanged, edge round skipped", t.ID) + return + } if !e.beginTaskOperation(t.ID) { return } @@ -895,6 +926,12 @@ func (e *Engine) plannerLoop(ctx context.Context, t *Task) { default: log.Printf("[planner] task %s planning finished", t.ID) } + // Record the post-round open-goal fingerprint for quiet-mode (#79): + // goals opened or closed by this round change it, so a later edge can + // tell "nothing changed" apart from "the plan input changed". + if fp, err := t.Store.OpenGoalFingerprint(); err == nil { + e.goalFP.Store(t.ID, fp) + } e.touch(t.ID) } diff --git a/server/planner_quiet_test.go b/server/planner_quiet_test.go new file mode 100644 index 0000000..fb067d5 --- /dev/null +++ b/server/planner_quiet_test.go @@ -0,0 +1,76 @@ +package server + +import ( + "testing" + + "github.com/Autumn-27/artex/db" +) + +// Planner quiet-mode (#79): an edge round is dropped only when BOTH hold — the +// open-goal fingerprint is unchanged since the last completed round AND active +// work exists (open or running intents). Every other combination plans. +func TestPlannerQuietSkipEdge(t *testing.T) { + m, err := NewManager(t.TempDir(), "") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = m.Close() }) + if m.pg == nil { + t.Skip("no postgres for this run") + } + + created, err := m.CreateTaskWithOptions("quiet-mode probe", "probe goal", db.TaskCreateOptions{}) + if err != nil { + t.Fatal(err) + } + tt, ok := m.Task(created.ID) + if !ok { + t.Fatal("task not found after creation") + } + store := tt.Store + e := NewEngine(m) + + gid, err := store.AddGoal(map[string]any{"summary": "g1"}, "seed") + if err != nil { + t.Fatal(err) + } + iid, err := store.AddIntent(map[string]any{"summary": "i1"}, 0, nil, "seed") + if err != nil { + t.Fatal(err) + } + fp, err := store.OpenGoalFingerprint() + if err != nil { + t.Fatal(err) + } + if fp == "" { + t.Fatal("fingerprint must not be empty with an open goal") + } + e.goalFP.Store(tt.ID, fp) + if !e.quietSkipEdge(tt) { + t.Fatal("unchanged goals + active intent must skip the edge round") + } + + // a goal closes -> the fingerprint changes -> plan + if err := store.SetNodeState(gid, "met"); err != nil { + t.Fatal(err) + } + if e.quietSkipEdge(tt) { + t.Fatal("a closed goal must allow planning") + } + + // goal back open, intent drained -> no active work -> plan + if err := store.SetNodeState(gid, "open"); err != nil { + t.Fatal(err) + } + fp2, err := store.OpenGoalFingerprint() + if err != nil { + t.Fatal(err) + } + e.goalFP.Store(tt.ID, fp2) + if err := store.SetIntentState(iid, "done"); err != nil { + t.Fatal(err) + } + if e.quietSkipEdge(tt) { + t.Fatal("no active intent must allow planning") + } +}