@trevonistrevon/pi-loop 0.4.6 → 0.4.8

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
@@ -271,11 +271,13 @@ export default function (pi) {
271
271
  });
272
272
  return true;
273
273
  }
274
- async function flushPendingNotifications() {
274
+ async function flushPendingNotifications(options) {
275
275
  if (flushPromise)
276
276
  return flushPromise;
277
277
  flushPromise = (async () => {
278
- if (agentRunning || _latestCtx?.hasPendingMessages())
278
+ if (agentRunning)
279
+ return;
280
+ if (!options?.ignorePendingMessages && _latestCtx?.hasPendingMessages())
279
281
  return;
280
282
  const entries = [...pendingNotifications.entries()]
281
283
  .sort(([, left], [, right]) => left.timestamp - right.timestamp);
@@ -375,7 +377,7 @@ export default function (pi) {
375
377
  agentRunning = false;
376
378
  _latestCtx = ctx;
377
379
  widget.setUICtx(ctx.ui);
378
- await flushPendingNotifications();
380
+ await flushPendingNotifications({ ignorePendingMessages: true });
379
381
  await pumpLoops();
380
382
  });
381
383
  pi.on("session_shutdown", async () => {
@@ -926,6 +928,34 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
926
928
  const active = loops.filter(l => l.status === "active").length;
927
929
  ui.notify(`${active}/${loops.length} active loops (max 25)`, "info");
928
930
  }
931
+ const AUTO_TASK_WORKER_THRESHOLD = 5;
932
+ 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.";
933
+ function findAutoTaskWorkerLoop() {
934
+ return store.list().find(entry => entry.status === "active"
935
+ && entry.prompt === AUTO_TASK_WORKER_PROMPT
936
+ && triggerHasEventSource(entry.trigger, "tasks:created"));
937
+ }
938
+ async function ensureAutoTaskWorkerLoop(taskStore) {
939
+ if (taskStore.pendingCount() < AUTO_TASK_WORKER_THRESHOLD)
940
+ return { created: false };
941
+ const existing = findAutoTaskWorkerLoop();
942
+ if (existing)
943
+ return { entry: existing, created: false };
944
+ const trigger = {
945
+ type: "hybrid",
946
+ cron: "*/5 * * * *",
947
+ event: { source: "tasks:created" },
948
+ debounceMs: 30000,
949
+ };
950
+ const entry = store.create(trigger, AUTO_TASK_WORKER_PROMPT, {
951
+ recurring: true,
952
+ maxFires: 30,
953
+ });
954
+ triggerSystem.add(entry);
955
+ await maybeBootstrapTaskLoop(entry);
956
+ widget.update();
957
+ return { entry, created: true };
958
+ }
929
959
  async function createNativeTaskInteractively(ui) {
930
960
  if (!nativeTaskStore) {
931
961
  ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
@@ -936,8 +966,18 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
936
966
  return;
937
967
  const description = await ui.input("Task description") || subject;
938
968
  const entry = nativeTaskStore.create(subject, description);
969
+ pi.events.emit("tasks:created", {
970
+ taskId: entry.id,
971
+ subject: entry.subject,
972
+ description: entry.description,
973
+ status: entry.status,
974
+ });
975
+ const worker = await ensureAutoTaskWorkerLoop(nativeTaskStore);
939
976
  widget.update();
940
977
  ui.notify(`Task #${entry.id} created`, "info");
978
+ if (worker.created && worker.entry) {
979
+ ui.notify(`Worker loop #${worker.entry.id} auto-created`, "info");
980
+ }
941
981
  }
942
982
  async function viewNativeTasks(ui) {
943
983
  if (!nativeTaskStore) {
@@ -1022,8 +1062,12 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
1022
1062
  description: entry.description,
1023
1063
  status: entry.status,
1024
1064
  });
1065
+ const worker = await ensureAutoTaskWorkerLoop(nativeTaskStore);
1025
1066
  widget.update();
1026
1067
  ctx.ui.notify(`Task #${entry.id} created`, "info");
1068
+ if (worker.created && worker.entry) {
1069
+ ctx.ui.notify(`Worker loop #${worker.entry.id} auto-created`, "info");
1070
+ }
1027
1071
  return;
1028
1072
  }
1029
1073
  await viewNativeTasks(ctx.ui);
@@ -1047,7 +1091,7 @@ Fields:
1047
1091
  subject: Type.String({ description: "Brief actionable title for the task" }),
1048
1092
  description: Type.String({ description: "Detailed description of what needs to be done" }),
1049
1093
  }),
1050
- execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
1094
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
1051
1095
  const entry = taskStore.create(params.subject, params.description);
1052
1096
  pi.events.emit("tasks:created", {
1053
1097
  taskId: entry.id,
@@ -1055,12 +1099,12 @@ Fields:
1055
1099
  description: entry.description,
1056
1100
  status: entry.status,
1057
1101
  });
1102
+ const worker = await ensureAutoTaskWorkerLoop(taskStore);
1058
1103
  widget.update();
1059
- const pending = taskStore.pendingCount();
1060
- const hint = pending >= 5
1061
- ? `\n(${pending} pending tasks — consider creating a worker loop: LoopCreate trigger='tasks:created' recurring: true maxFires: 30 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.')`
1104
+ const autoLoopMsg = worker.created && worker.entry
1105
+ ? `\nWorker loop #${worker.entry.id} auto-created`
1062
1106
  : "";
1063
- return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${hint}`));
1107
+ return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${autoLoopMsg}`));
1064
1108
  },
1065
1109
  });
1066
1110
  pi.registerTool({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trevonistrevon/pi-loop",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
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
@@ -292,11 +292,12 @@ export default function (pi: ExtensionAPI) {
292
292
  return true;
293
293
  }
294
294
 
295
- async function flushPendingNotifications(): Promise<void> {
295
+ async function flushPendingNotifications(options?: { ignorePendingMessages?: boolean }): Promise<void> {
296
296
  if (flushPromise) return flushPromise;
297
297
 
298
298
  flushPromise = (async () => {
299
- if (agentRunning || _latestCtx?.hasPendingMessages()) return;
299
+ if (agentRunning) return;
300
+ if (!options?.ignorePendingMessages && _latestCtx?.hasPendingMessages()) return;
300
301
 
301
302
  const entries = [...pendingNotifications.entries()]
302
303
  .sort(([, left], [, right]) => left.timestamp - right.timestamp);
@@ -408,7 +409,7 @@ export default function (pi: ExtensionAPI) {
408
409
  agentRunning = false;
409
410
  _latestCtx = ctx;
410
411
  widget.setUICtx(ctx.ui);
411
- await flushPendingNotifications();
412
+ await flushPendingNotifications({ ignorePendingMessages: true });
412
413
  await pumpLoops();
413
414
  });
414
415
 
@@ -1005,6 +1006,39 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
1005
1006
  ui.notify(`${active}/${loops.length} active loops (max 25)`, "info");
1006
1007
  }
1007
1008
 
1009
+ const AUTO_TASK_WORKER_THRESHOLD = 5;
1010
+ 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.";
1011
+
1012
+ function findAutoTaskWorkerLoop(): LoopEntry | undefined {
1013
+ return store.list().find(entry =>
1014
+ entry.status === "active"
1015
+ && entry.prompt === AUTO_TASK_WORKER_PROMPT
1016
+ && triggerHasEventSource(entry.trigger, "tasks:created")
1017
+ );
1018
+ }
1019
+
1020
+ async function ensureAutoTaskWorkerLoop(taskStore: TaskStore): Promise<{ entry?: LoopEntry; created: boolean }> {
1021
+ if (taskStore.pendingCount() < AUTO_TASK_WORKER_THRESHOLD) return { created: false };
1022
+
1023
+ const existing = findAutoTaskWorkerLoop();
1024
+ if (existing) return { entry: existing, created: false };
1025
+
1026
+ const trigger: Trigger = {
1027
+ type: "hybrid",
1028
+ cron: "*/5 * * * *",
1029
+ event: { source: "tasks:created" },
1030
+ debounceMs: 30000,
1031
+ };
1032
+ const entry = store.create(trigger, AUTO_TASK_WORKER_PROMPT, {
1033
+ recurring: true,
1034
+ maxFires: 30,
1035
+ });
1036
+ triggerSystem.add(entry);
1037
+ await maybeBootstrapTaskLoop(entry);
1038
+ widget.update();
1039
+ return { entry, created: true };
1040
+ }
1041
+
1008
1042
  async function createNativeTaskInteractively(ui: ExtensionUIContext) {
1009
1043
  if (!nativeTaskStore) {
1010
1044
  ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
@@ -1015,8 +1049,18 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
1015
1049
  if (!subject) return;
1016
1050
  const description = await ui.input("Task description") || subject;
1017
1051
  const entry = nativeTaskStore.create(subject, description);
1052
+ pi.events.emit("tasks:created", {
1053
+ taskId: entry.id,
1054
+ subject: entry.subject,
1055
+ description: entry.description,
1056
+ status: entry.status,
1057
+ });
1058
+ const worker = await ensureAutoTaskWorkerLoop(nativeTaskStore);
1018
1059
  widget.update();
1019
1060
  ui.notify(`Task #${entry.id} created`, "info");
1061
+ if (worker.created && worker.entry) {
1062
+ ui.notify(`Worker loop #${worker.entry.id} auto-created`, "info");
1063
+ }
1020
1064
  }
1021
1065
 
1022
1066
  async function viewNativeTasks(ui: ExtensionUIContext): Promise<void> {
@@ -1103,8 +1147,12 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
1103
1147
  description: entry.description,
1104
1148
  status: entry.status,
1105
1149
  });
1150
+ const worker = await ensureAutoTaskWorkerLoop(nativeTaskStore);
1106
1151
  widget.update();
1107
1152
  ctx.ui.notify(`Task #${entry.id} created`, "info");
1153
+ if (worker.created && worker.entry) {
1154
+ ctx.ui.notify(`Worker loop #${worker.entry.id} auto-created`, "info");
1155
+ }
1108
1156
  return;
1109
1157
  }
1110
1158
  await viewNativeTasks(ctx.ui);
@@ -1129,7 +1177,7 @@ Fields:
1129
1177
  subject: Type.String({ description: "Brief actionable title for the task" }),
1130
1178
  description: Type.String({ description: "Detailed description of what needs to be done" }),
1131
1179
  }),
1132
- execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
1180
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
1133
1181
  const entry = taskStore.create(params.subject, params.description);
1134
1182
  pi.events.emit("tasks:created", {
1135
1183
  taskId: entry.id,
@@ -1137,13 +1185,13 @@ Fields:
1137
1185
  description: entry.description,
1138
1186
  status: entry.status,
1139
1187
  });
1188
+ const worker = await ensureAutoTaskWorkerLoop(taskStore);
1140
1189
  widget.update();
1141
1190
 
1142
- const pending = taskStore.pendingCount();
1143
- const hint = pending >= 5
1144
- ? `\n(${pending} pending tasks — consider creating a worker loop: LoopCreate trigger='tasks:created' recurring: true maxFires: 30 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.')`
1191
+ const autoLoopMsg = worker.created && worker.entry
1192
+ ? `\nWorker loop #${worker.entry.id} auto-created`
1145
1193
  : "";
1146
- return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${hint}`));
1194
+ return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${autoLoopMsg}`));
1147
1195
  },
1148
1196
  });
1149
1197