@trevonistrevon/pi-loop 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.3](https://github.com/trvon/pi-loop/compare/pi-loop-v0.6.2...pi-loop-v0.6.3) (2026-07-17)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **loop:** prevent premature self-deletion ([6049e15](https://github.com/trvon/pi-loop/commit/6049e1599d9c6c1ab99a9eada72ec75307f25ca1))
9
+ * **ui:** restore loop status after resets ([da580f0](https://github.com/trvon/pi-loop/commit/da580f0d2f5ac414bc3cd884e27b757decd44b5d))
10
+
3
11
  ## [0.6.2](https://github.com/trvon/pi-loop/compare/pi-loop-v0.6.1...pi-loop-v0.6.2) (2026-07-17)
4
12
 
5
13
 
package/dist/index.js CHANGED
@@ -181,7 +181,9 @@ export default function (pi) {
181
181
  timestamp: Date.now(),
182
182
  readOnly: entry.readOnly,
183
183
  recurring: false,
184
+ persistent: entry.recurring,
184
185
  autoTask: true,
186
+ taskBacklog: entry.taskBacklog,
185
187
  });
186
188
  return true;
187
189
  }
@@ -250,7 +252,9 @@ export default function (pi) {
250
252
  timestamp: firedAt,
251
253
  readOnly: firedEntry.readOnly,
252
254
  recurring: firedEntry.recurring,
255
+ persistent: firedEntry.recurring,
253
256
  autoTask: firedEntry.autoTask,
257
+ taskBacklog: firedEntry.taskBacklog,
254
258
  dynamic: firedEntry.dynamic,
255
259
  });
256
260
  }
@@ -7,7 +7,9 @@ export interface ReducerNotification {
7
7
  timestamp: number;
8
8
  trigger: unknown;
9
9
  recurring?: boolean;
10
+ persistent?: boolean;
10
11
  autoTask?: boolean;
12
+ taskBacklog?: boolean;
11
13
  readOnly?: boolean;
12
14
  dynamic?: DynamicLoopState;
13
15
  }
@@ -7,7 +7,9 @@ export interface LoopFireEvent {
7
7
  timestamp: number;
8
8
  readOnly?: boolean;
9
9
  recurring?: boolean;
10
+ persistent?: boolean;
10
11
  autoTask?: boolean;
12
+ taskBacklog?: boolean;
11
13
  dynamic?: DynamicLoopState;
12
14
  }
13
15
  export interface PendingNotification extends LoopFireEvent {
@@ -63,12 +63,18 @@ export function createNotificationRuntime(options) {
63
63
  lines.push(`Metrics: ${dynamic.metrics}`);
64
64
  if (dynamic?.doneCriteria)
65
65
  lines.push(`Done criteria: ${dynamic.doneCriteria}`);
66
- lines.push("Continue toward the goal. When done call LoopUpdate with status=\"completed\". If more work remains, call LoopUpdate with status=\"continue\" plus state/metrics. Omit nextInterval for idle-driven rewake; include nextInterval for a timed wake. If blocked, use status=\"paused\".");
66
+ lines.push(`Loop lifecycle: Loop #${loopId} is the persistent controller for the overall goal. Do not call LoopDelete after this iteration.`, "Before ending this turn, call LoopUpdate exactly once: use status=\"completed\" only when the overall goal and done criteria are satisfied; use status=\"continue\" when any work remains, with state/metrics and optional nextInterval; use status=\"paused\" only when genuinely blocked. Omit nextInterval for an idle-driven rewake.");
67
67
  return lines.join("\n");
68
68
  }
69
+ const lifecycle = data.taskBacklog
70
+ ? `Backlog lifecycle: Loop #${loopId} is managed automatically. Do not call LoopDelete; when no pending tasks remain, report that and end this iteration.`
71
+ : (data.persistent ?? data.recurring)
72
+ ? `Loop lifecycle: Loop #${loopId} is recurring and remains active after this iteration. Do not call LoopDelete or pause it merely because this run finished, found no changes, or has no immediate work. Stop it only when the user or the loop prompt explicitly requires cancellation.`
73
+ : `Loop lifecycle: Loop #${loopId} is a one-shot wake and cleanup is automatic. Do not call LoopDelete.`;
69
74
  return [
70
75
  `[pi-loop] Loop #${loopId} fired (${triggerInfo}).${constraint}`,
71
76
  prompt,
77
+ lifecycle,
72
78
  ].join("\n");
73
79
  }
74
80
  function buildPendingNotification(data) {
@@ -97,8 +103,10 @@ export function createNotificationRuntime(options) {
97
103
  loopId: notification.loopId,
98
104
  trigger: notification.trigger,
99
105
  recurring: notification.recurring,
106
+ persistent: notification.persistent,
100
107
  readOnly: notification.readOnly,
101
108
  autoTask: notification.autoTask,
109
+ taskBacklog: notification.taskBacklog,
102
110
  dynamic: notification.dynamic,
103
111
  timestamp: notification.timestamp,
104
112
  },
@@ -15,8 +15,11 @@ export function registerSessionRuntimeHooks(options) {
15
15
  return;
16
16
  heartbeatTimer = setInterval(() => {
17
17
  // Swallow pump failures so a transient error never surfaces as an
18
- // unhandled rejection; the next tick retries.
19
- void pumpLoops().catch(() => { });
18
+ // unhandled rejection; repaint still runs so cleared harness UI heals.
19
+ void pumpLoops()
20
+ .catch(() => { })
21
+ .then(() => widget.update())
22
+ .catch(() => { });
20
23
  }, HEARTBEAT_MS);
21
24
  heartbeatTimer.unref?.();
22
25
  }
@@ -45,7 +48,6 @@ export function registerSessionRuntimeHooks(options) {
45
48
  getStore().expireEventLoops(sessionStartedAt);
46
49
  getTriggerSystem().start();
47
50
  ensureHeartbeat();
48
- widget.update();
49
51
  }
50
52
  }
51
53
  async function pumpLoops() {
@@ -66,6 +68,15 @@ export function registerSessionRuntimeHooks(options) {
66
68
  }
67
69
  getScheduler().pump(Date.now(), (entry) => !pendingTasks.has(entry.id));
68
70
  }
71
+ pi.on("session_start", async (_event, ctx) => {
72
+ setLatestCtx(ctx);
73
+ setSessionId(ctx.sessionManager.getSessionId());
74
+ widget.setUICtx(ctx.ui);
75
+ upgradeStoreIfNeeded(ctx);
76
+ ensureHeartbeat();
77
+ showPersistedLoops();
78
+ widget.update();
79
+ });
69
80
  pi.on("turn_start", async (_event, ctx) => {
70
81
  setLatestCtx(ctx);
71
82
  setSessionId(ctx.sessionManager.getSessionId());
@@ -2,7 +2,7 @@ import type { TaskStore } from "../task-store.js";
2
2
  import type { LoopDeletionTombstoneInput, LoopEntry, Trigger } from "../types.js";
3
3
  import { type LoopAutodeletedPayload, type TaskBacklogEmptyPayload } from "./loop-events.js";
4
4
  export declare const AUTO_TASK_WORKER_THRESHOLD = 5;
5
- export declare const AUTO_TASK_WORKER_PROMPT = "Run TaskList, pick next pending task, mark it in_progress, implement it, run validation, complete it. If no pending tasks remain, call LoopDelete on your own loop ID.";
5
+ export declare const AUTO_TASK_WORKER_PROMPT = "Run TaskList, pick next pending task, mark it in_progress, implement it, run validation, and complete it. If no pending tasks remain, report that and end this iteration; pi-loop manages the worker lifecycle automatically.";
6
6
  export interface TaskBacklogRuntimeOptions {
7
7
  getLoops: () => LoopEntry[];
8
8
  createLoop: (trigger: Trigger, prompt: string, options: {
@@ -2,7 +2,7 @@ import { createCoordinator, } from "../coordinator.js";
2
2
  import { reduceTaskBacklogEvent, } from "../task-backlog-coordinator.js";
3
3
  import { buildLoopAutodeletedPayload, buildTaskBacklogEmptyPayload, } from "./loop-events.js";
4
4
  export const AUTO_TASK_WORKER_THRESHOLD = 5;
5
- export const AUTO_TASK_WORKER_PROMPT = "Run TaskList, pick next pending task, mark it in_progress, implement it, run validation, complete it. If no pending tasks remain, call LoopDelete on your own loop ID.";
5
+ export const AUTO_TASK_WORKER_PROMPT = "Run TaskList, pick next pending task, mark it in_progress, implement it, run validation, and complete it. If no pending tasks remain, report that and end this iteration; pi-loop manages the worker lifecycle automatically.";
6
6
  export function createTaskBacklogRuntime(options) {
7
7
  const { getLoops, createLoop, deleteLoop, recordDeletionTombstone, addTrigger, removeTrigger, updateWidget, hasPendingTasks, bootstrapTaskLoop, triggerHasEventSource, emitLoopAutodeleted, emitTaskBacklogEmpty, debug, } = options;
8
8
  function isAutoTaskWorkerLoop(entry) {
@@ -138,7 +138,11 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
138
138
  - **autoTask**: when pi-tasks is loaded or native task fallback is active, auto-create a task on each fire
139
139
  - **taskBacklog**: mark this as a task-backlog worker loop so it auto-deletes when pending tasks reach zero
140
140
  - **readOnly**: restrict the agent to read-only tools when this loop fires (default: false)
141
- - **maxFires**: auto-stop after N fires — prevents infinite token burn on polling loops`,
141
+ - **maxFires**: auto-stop after N fires — prevents infinite token burn on polling loops
142
+
143
+ ## Loop Lifecycle
144
+
145
+ Recurring loops persist across fires. A completed iteration, unchanged result, or temporarily empty check is not a reason to delete the loop. Delete only when the user explicitly cancels it or its stated stop condition is satisfied. Dynamic loops must be advanced with LoopUpdate, not LoopDelete.`,
142
146
  promptGuidelines: [
143
147
  "Use LoopCreate when the user asks for a repeating task, periodic check, scheduled reminder, or 'every X' — never use raw Bash for/sleep/while.",
144
148
  "## Choosing trigger type",
@@ -150,14 +154,14 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
150
154
  "Default to 5m unless the user specifies differently. Use shorter intervals only when time-sensitive.",
151
155
  "## maxFires — prevent infinite token burn",
152
156
  "Always set maxFires on polling loops so they don't run forever. For task-continuation loops, use maxFires: 20-50.",
153
- "When a loop fires and finds nothing to do, call LoopDelete on its own ID to stop it don't keep polling.",
157
+ "Recurring loops are persistent controllers. Do not call LoopDelete after a normal fire, an unchanged check, or one completed iteration; only delete when the user explicitly asks to cancel or the loop's stated stop condition is satisfied.",
154
158
  "## readOnly mode",
155
159
  "Set readOnly: true for loops that only observe and report (checks, status polls). This prevents unintended changes.",
156
160
  "## Task-driven workflows",
157
161
  "Do not rely on a past 'tasks:created' event to replay. If tasks already exist, bootstrap the first pass in the current turn or use a hybrid/event loop that can catch future task creation and a cron safety-net.",
158
162
  "Use autoTask only when you want the loop itself to create a task on each fire. For processing an existing task backlog, leave autoTask off and have the loop run TaskList to pick the next pending task.",
159
163
  "Set taskBacklog: true for backlog worker loops that process the existing pending queue. Backlog worker loops bootstrap against existing pending tasks and auto-delete when the queue reaches zero.",
160
- "When no tasks are pending, the loop should stop itself or skip the wake entirely no tokens burned on empty polls.",
164
+ "For taskBacklog loops, do not instruct the agent to delete the loop; pi-loop auto-deletes it when the pending count reaches zero.",
161
165
  "After creating a loop, tell the user the loop ID so they can cancel it with LoopDelete.",
162
166
  ],
163
167
  parameters: Type.Object({
@@ -230,7 +234,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
230
234
  (entry.taskBacklog ? "Backlog worker: enabled\n" : "") +
231
235
  (bootstrapped ? "Backlog: initial wake queued for existing pending tasks\n" : "") +
232
236
  (isTaskSystemReady() ? "" : "Task system: not ready yet — autoTask may not fire until native fallback or pi-tasks becomes available\n") +
233
- `ID: ${entry.id} (use LoopDelete to cancel)`));
237
+ `ID: ${entry.id} (persists until explicitly canceled or a configured stop condition is met)`));
234
238
  },
235
239
  });
236
240
  pi.registerTool({
@@ -269,7 +273,7 @@ Use this before creating new loops to avoid duplicates, or to find IDs for LoopD
269
273
  label: "LoopUpdate",
270
274
  description: `Update progress for a dynamic loop.
271
275
 
272
- Use this after a dynamic loop wake. Mark status as "continue" with updated state/metrics and optional nextInterval, "completed" when the goal is done, or "paused" to stop temporarily.`,
276
+ Use this exactly once after each dynamic loop wake. Mark status as "continue" with updated state/metrics and optional nextInterval whenever any work remains, "completed" only when the overall goal and done criteria are satisfied, or "paused" when genuinely blocked. Do not use LoopDelete to finish an iteration.`,
273
277
  parameters: Type.Object({
274
278
  id: Type.String({ description: "Dynamic loop ID to update" }),
275
279
  status: Type.String({ description: "continue, completed, or paused", enum: ["continue", "completed", "paused"] }),
@@ -300,7 +304,9 @@ Use this after a dynamic loop wake. Mark status as "continue" with updated state
300
304
  label: "LoopDelete",
301
305
  description: `Delete or pause a loop by its ID.
302
306
 
303
- Use "pause" to temporarily stop a loop without removing it. Use "delete" to permanently remove it.`,
307
+ Use "pause" to temporarily stop a loop without removing it. Use "delete" to permanently remove it.
308
+
309
+ Do not use this after a normal loop fire, an unchanged check, an empty iteration, or one step of a dynamic goal. Recurring loops remain active across iterations; dynamic loops use LoopUpdate. Delete only when the user explicitly asks to cancel the loop or its stated stop condition is satisfied.`,
304
310
  parameters: Type.Object({
305
311
  id: Type.String({ description: "Loop ID to delete or pause" }),
306
312
  action: Type.Optional(Type.String({ description: "delete or pause (default: delete)", enum: ["delete", "pause"], default: "delete" })),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trevonistrevon/pi-loop",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "A pi extension for cron/event-based agent re-wake loops and background process monitoring.",
5
5
  "author": "trevonistrevon",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -200,7 +200,9 @@ export default function (pi: ExtensionAPI) {
200
200
  timestamp: Date.now(),
201
201
  readOnly: entry.readOnly,
202
202
  recurring: false,
203
+ persistent: entry.recurring,
203
204
  autoTask: true,
205
+ taskBacklog: entry.taskBacklog,
204
206
  });
205
207
  return true;
206
208
  }
@@ -276,7 +278,9 @@ export default function (pi: ExtensionAPI) {
276
278
  timestamp: firedAt,
277
279
  readOnly: firedEntry.readOnly,
278
280
  recurring: firedEntry.recurring,
281
+ persistent: firedEntry.recurring,
279
282
  autoTask: firedEntry.autoTask,
283
+ taskBacklog: firedEntry.taskBacklog,
280
284
  dynamic: firedEntry.dynamic,
281
285
  });
282
286
  }
@@ -9,7 +9,9 @@ export interface ReducerNotification {
9
9
  timestamp: number;
10
10
  trigger: unknown;
11
11
  recurring?: boolean;
12
+ persistent?: boolean;
12
13
  autoTask?: boolean;
14
+ taskBacklog?: boolean;
13
15
  readOnly?: boolean;
14
16
  dynamic?: DynamicLoopState;
15
17
  }
@@ -21,7 +21,9 @@ export interface LoopFireEvent {
21
21
  timestamp: number;
22
22
  readOnly?: boolean;
23
23
  recurring?: boolean;
24
+ persistent?: boolean;
24
25
  autoTask?: boolean;
26
+ taskBacklog?: boolean;
25
27
  dynamic?: DynamicLoopState;
26
28
  }
27
29
 
@@ -115,14 +117,22 @@ export function createNotificationRuntime(options: NotificationRuntimeOptions):
115
117
  if (dynamic?.metrics) lines.push(`Metrics: ${dynamic.metrics}`);
116
118
  if (dynamic?.doneCriteria) lines.push(`Done criteria: ${dynamic.doneCriteria}`);
117
119
  lines.push(
118
- "Continue toward the goal. When done call LoopUpdate with status=\"completed\". If more work remains, call LoopUpdate with status=\"continue\" plus state/metrics. Omit nextInterval for idle-driven rewake; include nextInterval for a timed wake. If blocked, use status=\"paused\".",
120
+ `Loop lifecycle: Loop #${loopId} is the persistent controller for the overall goal. Do not call LoopDelete after this iteration.`,
121
+ "Before ending this turn, call LoopUpdate exactly once: use status=\"completed\" only when the overall goal and done criteria are satisfied; use status=\"continue\" when any work remains, with state/metrics and optional nextInterval; use status=\"paused\" only when genuinely blocked. Omit nextInterval for an idle-driven rewake.",
119
122
  );
120
123
  return lines.join("\n");
121
124
  }
122
125
 
126
+ const lifecycle = data.taskBacklog
127
+ ? `Backlog lifecycle: Loop #${loopId} is managed automatically. Do not call LoopDelete; when no pending tasks remain, report that and end this iteration.`
128
+ : (data.persistent ?? data.recurring)
129
+ ? `Loop lifecycle: Loop #${loopId} is recurring and remains active after this iteration. Do not call LoopDelete or pause it merely because this run finished, found no changes, or has no immediate work. Stop it only when the user or the loop prompt explicitly requires cancellation.`
130
+ : `Loop lifecycle: Loop #${loopId} is a one-shot wake and cleanup is automatic. Do not call LoopDelete.`;
131
+
123
132
  return [
124
133
  `[pi-loop] Loop #${loopId} fired (${triggerInfo}).${constraint}`,
125
134
  prompt,
135
+ lifecycle,
126
136
  ].join("\n");
127
137
  }
128
138
 
@@ -154,8 +164,10 @@ export function createNotificationRuntime(options: NotificationRuntimeOptions):
154
164
  loopId: notification.loopId,
155
165
  trigger: notification.trigger,
156
166
  recurring: notification.recurring,
167
+ persistent: notification.persistent,
157
168
  readOnly: notification.readOnly,
158
169
  autoTask: notification.autoTask,
170
+ taskBacklog: notification.taskBacklog,
159
171
  dynamic: notification.dynamic,
160
172
  timestamp: notification.timestamp,
161
173
  },
@@ -62,8 +62,11 @@ export function registerSessionRuntimeHooks(options: SessionRuntimeOptions): voi
62
62
  if (heartbeatTimer) return;
63
63
  heartbeatTimer = setInterval(() => {
64
64
  // Swallow pump failures so a transient error never surfaces as an
65
- // unhandled rejection; the next tick retries.
66
- void pumpLoops().catch(() => {});
65
+ // unhandled rejection; repaint still runs so cleared harness UI heals.
66
+ void pumpLoops()
67
+ .catch(() => {})
68
+ .then(() => widget.update())
69
+ .catch(() => {});
67
70
  }, HEARTBEAT_MS);
68
71
  heartbeatTimer.unref?.();
69
72
  }
@@ -92,7 +95,6 @@ export function registerSessionRuntimeHooks(options: SessionRuntimeOptions): voi
92
95
  getStore().expireEventLoops(sessionStartedAt);
93
96
  getTriggerSystem().start();
94
97
  ensureHeartbeat();
95
- widget.update();
96
98
  }
97
99
  }
98
100
 
@@ -110,6 +112,16 @@ export function registerSessionRuntimeHooks(options: SessionRuntimeOptions): voi
110
112
  getScheduler().pump(Date.now(), (entry) => !pendingTasks.has(entry.id));
111
113
  }
112
114
 
115
+ pi.on("session_start", async (_event, ctx) => {
116
+ setLatestCtx(ctx);
117
+ setSessionId(ctx.sessionManager.getSessionId());
118
+ widget.setUICtx(ctx.ui);
119
+ upgradeStoreIfNeeded(ctx);
120
+ ensureHeartbeat();
121
+ showPersistedLoops();
122
+ widget.update();
123
+ });
124
+
113
125
  pi.on("turn_start", async (_event, ctx) => {
114
126
  setLatestCtx(ctx);
115
127
  setSessionId(ctx.sessionManager.getSessionId());
@@ -17,7 +17,7 @@ import {
17
17
  } from "./loop-events.js";
18
18
 
19
19
  export const AUTO_TASK_WORKER_THRESHOLD = 5;
20
- export const AUTO_TASK_WORKER_PROMPT = "Run TaskList, pick next pending task, mark it in_progress, implement it, run validation, complete it. If no pending tasks remain, call LoopDelete on your own loop ID.";
20
+ export const AUTO_TASK_WORKER_PROMPT = "Run TaskList, pick next pending task, mark it in_progress, implement it, run validation, and complete it. If no pending tasks remain, report that and end this iteration; pi-loop manages the worker lifecycle automatically.";
21
21
 
22
22
  export interface TaskBacklogRuntimeOptions {
23
23
  getLoops: () => LoopEntry[];
@@ -214,7 +214,11 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
214
214
  - **autoTask**: when pi-tasks is loaded or native task fallback is active, auto-create a task on each fire
215
215
  - **taskBacklog**: mark this as a task-backlog worker loop so it auto-deletes when pending tasks reach zero
216
216
  - **readOnly**: restrict the agent to read-only tools when this loop fires (default: false)
217
- - **maxFires**: auto-stop after N fires — prevents infinite token burn on polling loops`,
217
+ - **maxFires**: auto-stop after N fires — prevents infinite token burn on polling loops
218
+
219
+ ## Loop Lifecycle
220
+
221
+ Recurring loops persist across fires. A completed iteration, unchanged result, or temporarily empty check is not a reason to delete the loop. Delete only when the user explicitly cancels it or its stated stop condition is satisfied. Dynamic loops must be advanced with LoopUpdate, not LoopDelete.`,
218
222
  promptGuidelines: [
219
223
  "Use LoopCreate when the user asks for a repeating task, periodic check, scheduled reminder, or 'every X' — never use raw Bash for/sleep/while.",
220
224
  "## Choosing trigger type",
@@ -226,14 +230,14 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
226
230
  "Default to 5m unless the user specifies differently. Use shorter intervals only when time-sensitive.",
227
231
  "## maxFires — prevent infinite token burn",
228
232
  "Always set maxFires on polling loops so they don't run forever. For task-continuation loops, use maxFires: 20-50.",
229
- "When a loop fires and finds nothing to do, call LoopDelete on its own ID to stop it don't keep polling.",
233
+ "Recurring loops are persistent controllers. Do not call LoopDelete after a normal fire, an unchanged check, or one completed iteration; only delete when the user explicitly asks to cancel or the loop's stated stop condition is satisfied.",
230
234
  "## readOnly mode",
231
235
  "Set readOnly: true for loops that only observe and report (checks, status polls). This prevents unintended changes.",
232
236
  "## Task-driven workflows",
233
237
  "Do not rely on a past 'tasks:created' event to replay. If tasks already exist, bootstrap the first pass in the current turn or use a hybrid/event loop that can catch future task creation and a cron safety-net.",
234
238
  "Use autoTask only when you want the loop itself to create a task on each fire. For processing an existing task backlog, leave autoTask off and have the loop run TaskList to pick the next pending task.",
235
239
  "Set taskBacklog: true for backlog worker loops that process the existing pending queue. Backlog worker loops bootstrap against existing pending tasks and auto-delete when the queue reaches zero.",
236
- "When no tasks are pending, the loop should stop itself or skip the wake entirely no tokens burned on empty polls.",
240
+ "For taskBacklog loops, do not instruct the agent to delete the loop; pi-loop auto-deletes it when the pending count reaches zero.",
237
241
  "After creating a loop, tell the user the loop ID so they can cancel it with LoopDelete.",
238
242
  ],
239
243
  parameters: Type.Object({
@@ -312,7 +316,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
312
316
  (entry.taskBacklog ? "Backlog worker: enabled\n" : "") +
313
317
  (bootstrapped ? "Backlog: initial wake queued for existing pending tasks\n" : "") +
314
318
  (isTaskSystemReady() ? "" : "Task system: not ready yet — autoTask may not fire until native fallback or pi-tasks becomes available\n") +
315
- `ID: ${entry.id} (use LoopDelete to cancel)`
319
+ `ID: ${entry.id} (persists until explicitly canceled or a configured stop condition is met)`
316
320
  ));
317
321
  },
318
322
  });
@@ -354,7 +358,7 @@ Use this before creating new loops to avoid duplicates, or to find IDs for LoopD
354
358
  label: "LoopUpdate",
355
359
  description: `Update progress for a dynamic loop.
356
360
 
357
- Use this after a dynamic loop wake. Mark status as "continue" with updated state/metrics and optional nextInterval, "completed" when the goal is done, or "paused" to stop temporarily.`,
361
+ Use this exactly once after each dynamic loop wake. Mark status as "continue" with updated state/metrics and optional nextInterval whenever any work remains, "completed" only when the overall goal and done criteria are satisfied, or "paused" when genuinely blocked. Do not use LoopDelete to finish an iteration.`,
358
362
  parameters: Type.Object({
359
363
  id: Type.String({ description: "Dynamic loop ID to update" }),
360
364
  status: Type.String({ description: "continue, completed, or paused", enum: ["continue", "completed", "paused"] }),
@@ -386,7 +390,9 @@ Use this after a dynamic loop wake. Mark status as "continue" with updated state
386
390
  label: "LoopDelete",
387
391
  description: `Delete or pause a loop by its ID.
388
392
 
389
- Use "pause" to temporarily stop a loop without removing it. Use "delete" to permanently remove it.`,
393
+ Use "pause" to temporarily stop a loop without removing it. Use "delete" to permanently remove it.
394
+
395
+ Do not use this after a normal loop fire, an unchanged check, an empty iteration, or one step of a dynamic goal. Recurring loops remain active across iterations; dynamic loops use LoopUpdate. Delete only when the user explicitly asks to cancel the loop or its stated stop condition is satisfied.`,
390
396
  parameters: Type.Object({
391
397
  id: Type.String({ description: "Loop ID to delete or pause" }),
392
398
  action: Type.Optional(Type.String({ description: "delete or pause (default: delete)", enum: ["delete", "pause"], default: "delete" })),