Agent Zero AI framework
Stop idle goal polling
Publish goal revisions through the shared state monitor and refresh the Goal UI only when the selected chat or goal state changes. Keep the local elapsed-time clock, remove recurring API requests, and cover the state-push contract with focused regressions.
Alessandro committed
Aug 11, 2026 at 23:52 UTC
e8e566d221d654edc15e4e076a9dc992c2d483ba
6 files changed
+95
-13
plugins/_goal/AGENTS.md
+3
@@ -27,6 +27,9 @@
27
- `/goal auto` fills the composer with a prompt asking the agent to create and manage its own goal instead of silently sending a message.
28
- While a goal is active, response-tool calls are intermediate updates; only completing or blocking the goal restores normal loop termination.
29
- Goal UI feedback uses toast notifications and inline controls, not modal dialogs.
30
+- Goal state changes publish a context revision through the shared state-push
31
+ lifecycle; the WebUI refreshes on context or revision changes and never polls
32
+ the Goal API while idle.
33
34
## Work Guidance
35
plugins/_goal/extensions/webui/apply_snapshot_before/refresh-goal.js
new
+16
@@ -0,0 +1,16 @@
1
+import { store as goalStore } from "/plugins/_goal/webui/goal-store.js";
2
+
3
+let lastContextId = "";
4
+let lastRevision = null;
5
+
6
+export default async function refreshGoalOnRevision(ctx) {
7
+ const snapshot = ctx?.snapshot;
8
+ const contextId = String(snapshot?.context || "");
9
+ const activeContext = (snapshot?.contexts || []).find(item => item?.id === contextId);
10
+ const revision = activeContext?._goal_revision ?? null;
11
+
12
+ if (contextId === lastContextId && revision === lastRevision) return;
13
+ lastContextId = contextId;
14
+ lastRevision = revision;
15
+ await goalStore.refresh(true);
16
+}
plugins/_goal/extensions/webui/chat-input-progress-start/goal-strip.html
-1
@@ -5,7 +5,6 @@
5
<div x-data
6
class="goal-strip-root"
7
x-create="$store.goalBar.onMount()"
8
- x-init="$watch('$store.chats.selected', () => $store.goalBar.refresh(true))"
8
x-destroy="$store.goalBar.cleanup()">
9
<template x-if="$store.goalBar && $store.goalBar.visible">
10
<div class="goal-strip" :class="`is-${$store.goalBar.goal?.status || 'active'}`">
plugins/_goal/tests/test_goal_plugin.py
+51
@@ -1,6 +1,7 @@
1
from __future__ import annotations
2
3
import uuid
4
+from pathlib import Path
5
from types import SimpleNamespace
6
7
import pytest
@@ -57,6 +58,56 @@ def test_goal_storage_round_trip(context_id: str):
58
assert goal.get_goal(context_id) is None
59
60
61
+def test_goal_changes_publish_state_revision(context_id: str, monkeypatch):
62
+ from agent import AgentContext
63
+ from helpers import state_monitor_integration
64
+
65
+ revisions = iter([1.0, 2.0, 3.0])
66
+ output_data = {}
67
+ dirty = []
68
+ context = SimpleNamespace(
69
+ set_output_data=lambda key, value: output_data.__setitem__(key, value)
70
+ )
71
+ monkeypatch.setattr(AgentContext, "get", lambda _context_id: context)
72
+ monkeypatch.setattr(goal.time, "time", lambda: next(revisions))
73
+ monkeypatch.setattr(
74
+ state_monitor_integration,
75
+ "mark_dirty_for_context",
76
+ lambda context_id, *, reason: dirty.append((context_id, reason)),
77
+ )
78
+
79
+ goal.create_goal(context_id, "Publish changes")
80
+ goal.update_goal(context_id, status="paused")
81
+ goal.delete_goal(context_id)
82
+
83
+ assert output_data["_goal_revision"] == 3.0
84
+ assert dirty == [(context_id, "plugins._goal")] * 3
85
+
86
+
87
+def test_goal_webui_uses_state_revisions_instead_of_polling():
88
+ plugin_root = Path(__file__).resolve().parents[1]
89
+ store = (plugin_root / "webui" / "goal-store.js").read_text()
90
+ strip = (
91
+ plugin_root
92
+ / "extensions"
93
+ / "webui"
94
+ / "chat-input-progress-start"
95
+ / "goal-strip.html"
96
+ ).read_text()
97
+ refresh = (
98
+ plugin_root
99
+ / "extensions"
100
+ / "webui"
101
+ / "apply_snapshot_before"
102
+ / "refresh-goal.js"
103
+ ).read_text()
104
+
105
+ assert "setInterval(() => this.refresh" not in store
106
+ assert "$watch('$store.chats.selected'" not in strip
107
+ assert "_goal_revision" in refresh
108
+ assert "goalStore.refresh(true)" in refresh
109
+
110
+
111
def test_goal_command_sets_pauses_resumes_and_deletes(context_id: str):
112
created = goal_command.run(_payload(context_id, "/goal Add current goal support"))
113
assert created["effects"][0]["message"] == "Goal set."
plugins/_goal/tools/goal.py
+21
-1
@@ -2,6 +2,7 @@ from __future__ import annotations
2
3
import json
4
import re
5
+import time
6
from datetime import datetime, timezone
7
from pathlib import Path
8
from typing import Any
@@ -141,7 +142,9 @@ def update_goal(
142
143
144
def delete_goal(context_id: str) -> None:
144
- files.delete_file(_goal_path(_require_context_id(context_id)))
145
+ context_id = _require_context_id(context_id)
146
+ files.delete_file(_goal_path(context_id))
147
+ _notify_goal_changed(context_id)
148
149
150
def public_goal(goal: dict[str, Any] | None) -> dict[str, Any] | None:
@@ -182,6 +185,23 @@ def _write_goal(goal: dict[str, Any]) -> None:
185
_goal_path(str(goal["context_id"])),
186
json.dumps(public_goal(goal), indent=2, ensure_ascii=False) + "\n",
187
)
188
+ _notify_goal_changed(str(goal["context_id"]))
189
+
190
+
191
+def _notify_goal_changed(context_id: str) -> None:
192
+ from agent import AgentContext
193
+
194
+ context = AgentContext.get(context_id)
195
+ if context is None:
196
+ return
197
+ context.set_output_data("_goal_revision", time.time())
198
+
199
+ try:
200
+ from helpers.state_monitor_integration import mark_dirty_for_context
201
+
202
+ mark_dirty_for_context(context_id, reason="plugins._goal")
203
+ except Exception:
204
+ pass
205
206
207
def _normalize_goal(raw: dict[str, Any], *, context_id: str) -> dict[str, Any]:
plugins/_goal/webui/goal-store.js
+4
-11
@@ -15,7 +15,6 @@ const model = {
15
editing: false,
16
draft: "",
17
lastContextId: "",
18
- intervalId: null,
18
clockIntervalId: null,
19
goalChangedHandler: null,
20
now: Date.now(),
@@ -82,17 +81,15 @@ const model = {
81
document.getElementById("progress-bar-box")?.classList.add("has-goal-bar");
82
this.goalChangedHandler = (event) => {
83
const detail = event?.detail || {};
85
- if (detail.goal === null) {
86
- this.goal = null;
87
- }
88
- void this.refresh(true);
84
+ if (detail.context_id && detail.context_id !== this.contextId) return;
85
+ this.goal = detail.goal || null;
86
+ this.now = Date.now();
87
+ if (!this.goal) this.editing = false;
88
};
89
window.addEventListener("goal:changed", this.goalChangedHandler);
90
this.clockIntervalId = window.setInterval(() => {
91
this.now = Date.now();
92
}, 1000);
94
- this.intervalId = window.setInterval(() => this.refresh(), 3000);
95
- void this.refresh(true);
93
},
94
95
cleanup() {
@@ -100,14 +97,10 @@ const model = {
97
if (this.goalChangedHandler) {
98
window.removeEventListener("goal:changed", this.goalChangedHandler);
99
}
103
- if (this.intervalId) {
104
- window.clearInterval(this.intervalId);
105
- }
100
if (this.clockIntervalId) {
101
window.clearInterval(this.clockIntervalId);
102
}
103
this.goalChangedHandler = null;
110
- this.intervalId = null;
104
this.clockIntervalId = null;
105
},
106