Resume goals after terminal edits
Detect complete or blocked goals that are reactivated by an edit and submit the revised objective so agent processing restarts. Cover both terminal states and preserve silent edits for already-active goals.
Alessandro committed
Jul 16, 2026 at 20:42 UTC
81e5f2872e27a78c518047474459c6663f287726
4 files changed
+47
-2
plugins/_goal/AGENTS.md
+1
@@ -22,6 +22,7 @@
22
- Active goals are injected into agent extras; paused and blocked goals remain visible in the UI, while complete goals are hidden.
23
- Goal records track accumulated active time with `elapsed_seconds` and `active_since`; pausing freezes elapsed time until resume.
24
- User controls may pause, resume, edit, or delete a goal; destructive delete uses inline confirmation. Model tools may create goals and mark them complete or blocked.
25
+- Saving an edit that reactivates a complete or blocked goal resends the edited objective so agent processing resumes.
26
- `/goal <objective>` creates the goal and sends the objective as the user message so the agent starts working immediately.
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.
plugins/_goal/api/goal.py
+10
-1
@@ -41,6 +41,7 @@ class Goal(ApiHandler):
41
return {"ok": True, "goal": goals.public_goal(goal)}
42
43
def _update(self, context_id: str, input: dict) -> dict:
44
+ current = goals.get_goal(context_id)
45
goal = goals.update_goal(
46
context_id,
47
objective=input.get("objective") if "objective" in input else None,
@@ -48,7 +49,15 @@ class Goal(ApiHandler):
49
note=input.get("note") if "note" in input else None,
50
token_budget=input.get("token_budget") if "token_budget" in input else None,
51
)
51
- return {"ok": True, "goal": goals.public_goal(goal)}
52
+ return {
53
+ "ok": True,
54
+ "goal": goals.public_goal(goal),
55
+ "reactivated": (
56
+ current is not None
57
+ and current.get("status") in goals.FINAL_STATUSES
58
+ and goal.get("status") == "active"
59
+ ),
60
+ }
61
62
def _status(self, context_id: str, status: str) -> dict:
63
goal = goals.update_goal(context_id, status=status)
plugins/_goal/tests/test_goal_plugin.py
+24
@@ -125,6 +125,30 @@ async def test_goal_api_and_agent_tools(context_id: str):
125
assert goals.get_goal(context_id)["created_by"] == "model"
126
127
128
+@pytest.mark.parametrize("terminal_status", ["blocked", "complete"])
129
+@pytest.mark.asyncio
130
+async def test_editing_terminal_goal_requests_agent_reactivation(
131
+ context_id: str,
132
+ terminal_status: str,
133
+):
134
+ goals.create_goal(context_id, "Initial goal")
135
+ goals.update_goal(context_id, status=terminal_status)
136
+
137
+ response = await object.__new__(Goal).process(
138
+ {
139
+ "action": "update",
140
+ "context_id": context_id,
141
+ "objective": "Continue with the edited goal",
142
+ "status": "active",
143
+ },
144
+ None,
145
+ )
146
+
147
+ assert response["reactivated"] is True
148
+ assert response["goal"]["objective"] == "Continue with the edited goal"
149
+ assert response["goal"]["status"] == "active"
150
+
151
+
152
@pytest.mark.asyncio
153
async def test_active_goal_keeps_response_tool_running(context_id: str):
154
goals.create_goal(context_id, "Keep going")
plugins/_goal/webui/goal-store.js
+12
-1
@@ -160,8 +160,17 @@ const model = {
160
void toastFrontendError("Goal objective is required.", "Goal");
161
return;
162
}
163
- await this.update({ action: "update", objective, status: "active" }, "Goal updated.");
163
+ const response = await this.update(
164
+ { action: "update", objective, status: "active" },
165
+ "Goal updated.",
166
+ );
167
this.editing = false;
168
+ if (response?.reactivated) {
169
+ await globalThis.sendMessage?.({
170
+ message: response.goal?.objective || objective,
171
+ context: response.goal?.context_id || this.contextId,
172
+ });
173
+ }
174
},
175
176
async pauseOrResume() {
@@ -194,9 +203,11 @@ const model = {
203
detail: { goal: this.goal, context_id: contextId },
204
}));
205
void toastFrontendSuccess(successMessage, "Goal");
206
+ return response;
207
} catch (error) {
208
console.error("Failed to update goal:", error);
209
void toastFrontendError(error?.message || "Failed to update goal.", "Goal");
210
+ return null;
211
} finally {
212
this.saving = false;
213
}