@trevonistrevon/pi-loop 0.4.2 → 0.4.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/dist/index.js CHANGED
@@ -209,6 +209,35 @@ export default function (pi) {
209
209
  message: buildLoopFireMessage(data),
210
210
  };
211
211
  }
212
+ function triggerHasEventSource(trigger, source) {
213
+ if (typeof trigger === "string")
214
+ return false;
215
+ return trigger.type === "event"
216
+ ? trigger.source === source
217
+ : trigger.type === "hybrid"
218
+ ? trigger.event.source === source
219
+ : false;
220
+ }
221
+ async function maybeBootstrapTaskLoop(entry) {
222
+ if (!entry.recurring)
223
+ return false;
224
+ if (!triggerHasEventSource(entry.trigger, "tasks:created"))
225
+ return false;
226
+ const pending = await hasPendingTasks();
227
+ if (pending <= 0)
228
+ return false;
229
+ debug(`loop #${entry.id} — bootstrapping existing pending tasks (${pending})`);
230
+ await queueOrDeliverNotification({
231
+ loopId: entry.id,
232
+ prompt: entry.prompt,
233
+ trigger: entry.trigger,
234
+ timestamp: Date.now(),
235
+ readOnly: entry.readOnly,
236
+ recurring: false,
237
+ autoTask: true,
238
+ });
239
+ return true;
240
+ }
212
241
  async function deliverNotification(notification) {
213
242
  if (notification.autoTask) {
214
243
  const pending = await hasPendingTasks();
@@ -425,8 +454,9 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
425
454
  "## readOnly mode",
426
455
  "Set readOnly: true for loops that only observe and report (checks, status polls). This prevents unintended changes.",
427
456
  "## Task-driven workflows",
428
- "After creating tasks with TaskCreate, use an event loop with autoTask: true so the system checks for pending tasks before firing: LoopCreate trigger='tasks:created' triggerType='event' autoTask: true maxFires: 30 prompt='Run TaskList, pick the next available pending task, work on it.'",
429
- "When no tasks are pending, the loop skips the wake entirely no tokens burned on empty polls.",
457
+ "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.",
458
+ "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.",
459
+ "When no tasks are pending, the loop should stop itself or skip the wake entirely — no tokens burned on empty polls.",
430
460
  "After creating a loop, tell the user the loop ID so they can cancel it with LoopDelete.",
431
461
  ],
432
462
  parameters: Type.Object({
@@ -439,7 +469,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
439
469
  readOnly: Type.Optional(Type.Boolean({ description: "Restrict the agent to read-only tools when this loop fires (default: false)", default: false })),
440
470
  maxFires: Type.Optional(Type.Number({ description: "Auto-stop after N fires. Prevents infinite token burn on polling loops." })),
441
471
  }),
442
- execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
472
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
443
473
  const { trigger: triggerInput, prompt, recurring, autoTask, triggerType, debounceMs, readOnly, maxFires } = params;
444
474
  let trigger;
445
475
  const inferred = triggerType ?? inferTriggerType(triggerInput);
@@ -487,6 +517,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
487
517
  }
488
518
  catch { /* filter parse failure, ignore */ }
489
519
  }
520
+ const bootstrapped = await maybeBootstrapTaskLoop(entry);
490
521
  widget.update();
491
522
  const triggerDesc = trigger.type === "cron"
492
523
  ? `schedule: ${trigger.schedule}`
@@ -497,6 +528,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
497
528
  `Trigger: ${triggerDesc}\n` +
498
529
  `Recurring: ${entry.recurring}\n` +
499
530
  (entry.autoTask ? `Auto-task: enabled\n` : "") +
531
+ (bootstrapped ? "Bootstrap: queued initial wake for existing pending tasks\n" : "") +
500
532
  ((tasksAvailable || nativeTasksRegistered) ? "" : "(task system not ready yet — autoTask may not fire until native fallback or pi-tasks becomes available)\n") +
501
533
  `ID: ${entry.id} (use LoopDelete to cancel)`));
502
534
  },
@@ -956,6 +988,12 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
956
988
  }
957
989
  if (trimmed) {
958
990
  const entry = nativeTaskStore.create(trimmed.slice(0, 80), trimmed);
991
+ pi.events.emit("tasks:created", {
992
+ taskId: entry.id,
993
+ subject: entry.subject,
994
+ description: entry.description,
995
+ status: entry.status,
996
+ });
959
997
  widget.update();
960
998
  ctx.ui.notify(`Task #${entry.id} created`, "info");
961
999
  return;
@@ -982,6 +1020,12 @@ Fields:
982
1020
  }),
983
1021
  execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
984
1022
  const entry = taskStore.create(params.subject, params.description);
1023
+ pi.events.emit("tasks:created", {
1024
+ taskId: entry.id,
1025
+ subject: entry.subject,
1026
+ description: entry.description,
1027
+ status: entry.status,
1028
+ });
985
1029
  widget.update();
986
1030
  return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}`));
987
1031
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trevonistrevon/pi-loop",
3
- "version": "0.4.2",
3
+ "version": "0.4.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
@@ -229,6 +229,35 @@ export default function (pi: ExtensionAPI) {
229
229
  };
230
230
  }
231
231
 
232
+ function triggerHasEventSource(trigger: Trigger | string, source: string): boolean {
233
+ if (typeof trigger === "string") return false;
234
+ return trigger.type === "event"
235
+ ? trigger.source === source
236
+ : trigger.type === "hybrid"
237
+ ? trigger.event.source === source
238
+ : false;
239
+ }
240
+
241
+ async function maybeBootstrapTaskLoop(entry: LoopEntry): Promise<boolean> {
242
+ if (!entry.recurring) return false;
243
+ if (!triggerHasEventSource(entry.trigger, "tasks:created")) return false;
244
+
245
+ const pending = await hasPendingTasks();
246
+ if (pending <= 0) return false;
247
+
248
+ debug(`loop #${entry.id} — bootstrapping existing pending tasks (${pending})`);
249
+ await queueOrDeliverNotification({
250
+ loopId: entry.id,
251
+ prompt: entry.prompt,
252
+ trigger: entry.trigger,
253
+ timestamp: Date.now(),
254
+ readOnly: entry.readOnly,
255
+ recurring: false,
256
+ autoTask: true,
257
+ });
258
+ return true;
259
+ }
260
+
232
261
  async function deliverNotification(notification: PendingNotification): Promise<boolean> {
233
262
  if (notification.autoTask) {
234
263
  const pending = await hasPendingTasks();
@@ -470,8 +499,9 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
470
499
  "## readOnly mode",
471
500
  "Set readOnly: true for loops that only observe and report (checks, status polls). This prevents unintended changes.",
472
501
  "## Task-driven workflows",
473
- "After creating tasks with TaskCreate, use an event loop with autoTask: true so the system checks for pending tasks before firing: LoopCreate trigger='tasks:created' triggerType='event' autoTask: true maxFires: 30 prompt='Run TaskList, pick the next available pending task, work on it.'",
474
- "When no tasks are pending, the loop skips the wake entirely no tokens burned on empty polls.",
502
+ "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.",
503
+ "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.",
504
+ "When no tasks are pending, the loop should stop itself or skip the wake entirely — no tokens burned on empty polls.",
475
505
  "After creating a loop, tell the user the loop ID so they can cancel it with LoopDelete.",
476
506
  ],
477
507
  parameters: Type.Object({
@@ -485,7 +515,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
485
515
  maxFires: Type.Optional(Type.Number({ description: "Auto-stop after N fires. Prevents infinite token burn on polling loops." })),
486
516
  }),
487
517
 
488
- execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
518
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
489
519
  const { trigger: triggerInput, prompt, recurring, autoTask, triggerType, debounceMs, readOnly, maxFires } = params;
490
520
 
491
521
  let trigger: Trigger;
@@ -536,6 +566,8 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
536
566
  } catch { /* filter parse failure, ignore */ }
537
567
  }
538
568
 
569
+ const bootstrapped = await maybeBootstrapTaskLoop(entry);
570
+
539
571
  widget.update();
540
572
 
541
573
  const triggerDesc = trigger.type === "cron"
@@ -549,6 +581,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
549
581
  `Trigger: ${triggerDesc}\n` +
550
582
  `Recurring: ${entry.recurring}\n` +
551
583
  (entry.autoTask ? `Auto-task: enabled\n` : "") +
584
+ (bootstrapped ? "Bootstrap: queued initial wake for existing pending tasks\n" : "") +
552
585
  ((tasksAvailable || nativeTasksRegistered) ? "" : "(task system not ready yet — autoTask may not fire until native fallback or pi-tasks becomes available)\n") +
553
586
  `ID: ${entry.id} (use LoopDelete to cancel)`
554
587
  ));
@@ -1040,6 +1073,12 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
1040
1073
  }
1041
1074
  if (trimmed) {
1042
1075
  const entry = nativeTaskStore.create(trimmed.slice(0, 80), trimmed);
1076
+ pi.events.emit("tasks:created", {
1077
+ taskId: entry.id,
1078
+ subject: entry.subject,
1079
+ description: entry.description,
1080
+ status: entry.status,
1081
+ });
1043
1082
  widget.update();
1044
1083
  ctx.ui.notify(`Task #${entry.id} created`, "info");
1045
1084
  return;
@@ -1067,6 +1106,12 @@ Fields:
1067
1106
  }),
1068
1107
  execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
1069
1108
  const entry = taskStore.create(params.subject, params.description);
1109
+ pi.events.emit("tasks:created", {
1110
+ taskId: entry.id,
1111
+ subject: entry.subject,
1112
+ description: entry.description,
1113
+ status: entry.status,
1114
+ });
1070
1115
  widget.update();
1071
1116
  return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}`));
1072
1117
  },