@trevonistrevon/pi-loop 0.4.5 → 0.4.7

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/README.md CHANGED
@@ -107,6 +107,8 @@ Only task counts and the single active/next task are shown in the widget so atte
107
107
  | `PI_LOOP_SCOPE` | `memory` (ephemeral), `session` (per-session file), `project` (shared) | `session` |
108
108
  | `PI_LOOP_DEBUG` | Debug logging to stderr | unset |
109
109
 
110
+ In `session` scope (default), loop and task files are saved per session ID (e.g. `.pi/tasks/tasks-<sessionId>.json`) so concurrent sessions and worktree agents do not share state. In `memory` scope nothing persists to disk.
111
+
110
112
  ## Limits
111
113
 
112
114
  25 active loops, 25 running monitors. Recurring loops expire after 7 days.
package/dist/index.js CHANGED
@@ -53,9 +53,14 @@ export default function (pi) {
53
53
  return undefined;
54
54
  return join(process.cwd(), ".pi", "loops", "loops.json");
55
55
  }
56
- function resolveTaskStorePath() {
56
+ function resolveTaskStorePath(sessionId) {
57
57
  if (loopScope === "memory")
58
58
  return undefined;
59
+ if (loopScope === "session" && sessionId) {
60
+ return join(process.cwd(), ".pi", "tasks", `tasks-${sessionId}.json`);
61
+ }
62
+ if (loopScope === "session")
63
+ return undefined;
59
64
  return join(process.cwd(), ".pi", "tasks", "tasks.json");
60
65
  }
61
66
  let store = new LoopStore(resolveStorePath());
@@ -319,6 +324,7 @@ export default function (pi) {
319
324
  let storeUpgraded = false;
320
325
  let persistedShown = false;
321
326
  let _latestCtx;
327
+ let _sessionId;
322
328
  function upgradeStoreIfNeeded(ctx) {
323
329
  if (storeUpgraded)
324
330
  return;
@@ -347,6 +353,7 @@ export default function (pi) {
347
353
  }
348
354
  pi.on("turn_start", async (_event, ctx) => {
349
355
  _latestCtx = ctx;
356
+ _sessionId = ctx.sessionManager.getSessionId();
350
357
  widget.setUICtx(ctx.ui);
351
358
  upgradeStoreIfNeeded(ctx);
352
359
  widget.update();
@@ -381,6 +388,7 @@ export default function (pi) {
381
388
  triggerSystem.stop();
382
389
  agentRunning = false;
383
390
  pendingNotifications.clear();
391
+ _sessionId = undefined;
384
392
  const isResume = event?.reason === "resume";
385
393
  storeUpgraded = false;
386
394
  persistedShown = false;
@@ -918,6 +926,34 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
918
926
  const active = loops.filter(l => l.status === "active").length;
919
927
  ui.notify(`${active}/${loops.length} active loops (max 25)`, "info");
920
928
  }
929
+ const AUTO_TASK_WORKER_THRESHOLD = 5;
930
+ 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.";
931
+ function findAutoTaskWorkerLoop() {
932
+ return store.list().find(entry => entry.status === "active"
933
+ && entry.prompt === AUTO_TASK_WORKER_PROMPT
934
+ && triggerHasEventSource(entry.trigger, "tasks:created"));
935
+ }
936
+ async function ensureAutoTaskWorkerLoop(taskStore) {
937
+ if (taskStore.pendingCount() < AUTO_TASK_WORKER_THRESHOLD)
938
+ return { created: false };
939
+ const existing = findAutoTaskWorkerLoop();
940
+ if (existing)
941
+ return { entry: existing, created: false };
942
+ const trigger = {
943
+ type: "hybrid",
944
+ cron: "*/5 * * * *",
945
+ event: { source: "tasks:created" },
946
+ debounceMs: 30000,
947
+ };
948
+ const entry = store.create(trigger, AUTO_TASK_WORKER_PROMPT, {
949
+ recurring: true,
950
+ maxFires: 30,
951
+ });
952
+ triggerSystem.add(entry);
953
+ await maybeBootstrapTaskLoop(entry);
954
+ widget.update();
955
+ return { entry, created: true };
956
+ }
921
957
  async function createNativeTaskInteractively(ui) {
922
958
  if (!nativeTaskStore) {
923
959
  ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
@@ -928,8 +964,18 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
928
964
  return;
929
965
  const description = await ui.input("Task description") || subject;
930
966
  const entry = nativeTaskStore.create(subject, description);
967
+ pi.events.emit("tasks:created", {
968
+ taskId: entry.id,
969
+ subject: entry.subject,
970
+ description: entry.description,
971
+ status: entry.status,
972
+ });
973
+ const worker = await ensureAutoTaskWorkerLoop(nativeTaskStore);
931
974
  widget.update();
932
975
  ui.notify(`Task #${entry.id} created`, "info");
976
+ if (worker.created && worker.entry) {
977
+ ui.notify(`Worker loop #${worker.entry.id} auto-created`, "info");
978
+ }
933
979
  }
934
980
  async function viewNativeTasks(ui) {
935
981
  if (!nativeTaskStore) {
@@ -995,7 +1041,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
995
1041
  setTimeout(async () => {
996
1042
  if (tasksAvailable || nativeTasksRegistered)
997
1043
  return;
998
- nativeTaskStore = new TaskStore(resolveTaskStorePath());
1044
+ nativeTaskStore = new TaskStore(resolveTaskStorePath(_sessionId));
999
1045
  nativeTasksRegistered = true;
1000
1046
  const taskStore = nativeTaskStore;
1001
1047
  pi.registerCommand("tasks", {
@@ -1014,8 +1060,12 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
1014
1060
  description: entry.description,
1015
1061
  status: entry.status,
1016
1062
  });
1063
+ const worker = await ensureAutoTaskWorkerLoop(nativeTaskStore);
1017
1064
  widget.update();
1018
1065
  ctx.ui.notify(`Task #${entry.id} created`, "info");
1066
+ if (worker.created && worker.entry) {
1067
+ ctx.ui.notify(`Worker loop #${worker.entry.id} auto-created`, "info");
1068
+ }
1019
1069
  return;
1020
1070
  }
1021
1071
  await viewNativeTasks(ctx.ui);
@@ -1032,13 +1082,14 @@ Fields:
1032
1082
  - metadata: optional tags/metadata`,
1033
1083
  promptGuidelines: [
1034
1084
  "Use TaskCreate to track complex multi-step work across turns.",
1085
+ "Break work into small, independently completable tasks. A task should be finishable in one focused session — if a task would take multiple turns, split it further.",
1035
1086
  "TaskCreate accepts `subject` and `description` parameters only — do not invent extra fields unless the schema explicitly adds them.",
1036
1087
  ],
1037
1088
  parameters: Type.Object({
1038
1089
  subject: Type.String({ description: "Brief actionable title for the task" }),
1039
1090
  description: Type.String({ description: "Detailed description of what needs to be done" }),
1040
1091
  }),
1041
- execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
1092
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
1042
1093
  const entry = taskStore.create(params.subject, params.description);
1043
1094
  pi.events.emit("tasks:created", {
1044
1095
  taskId: entry.id,
@@ -1046,12 +1097,12 @@ Fields:
1046
1097
  description: entry.description,
1047
1098
  status: entry.status,
1048
1099
  });
1100
+ const worker = await ensureAutoTaskWorkerLoop(taskStore);
1049
1101
  widget.update();
1050
- const pending = taskStore.pendingCount();
1051
- const hint = pending >= 5
1052
- ? `\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.')`
1102
+ const autoLoopMsg = worker.created && worker.entry
1103
+ ? `\nWorker loop #${worker.entry.id} auto-created`
1053
1104
  : "";
1054
- return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${hint}`));
1105
+ return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${autoLoopMsg}`));
1055
1106
  },
1056
1107
  });
1057
1108
  pi.registerTool({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trevonistrevon/pi-loop",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
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
@@ -73,8 +73,12 @@ export default function (pi: ExtensionAPI) {
73
73
  return join(process.cwd(), ".pi", "loops", "loops.json");
74
74
  }
75
75
 
76
- function resolveTaskStorePath(): string | undefined {
76
+ function resolveTaskStorePath(sessionId?: string): string | undefined {
77
77
  if (loopScope === "memory") return undefined;
78
+ if (loopScope === "session" && sessionId) {
79
+ return join(process.cwd(), ".pi", "tasks", `tasks-${sessionId}.json`);
80
+ }
81
+ if (loopScope === "session") return undefined;
78
82
  return join(process.cwd(), ".pi", "tasks", "tasks.json");
79
83
  }
80
84
 
@@ -349,6 +353,7 @@ export default function (pi: ExtensionAPI) {
349
353
  let storeUpgraded = false;
350
354
  let persistedShown = false;
351
355
  let _latestCtx: ExtensionContext | undefined;
356
+ let _sessionId: string | undefined;
352
357
 
353
358
  function upgradeStoreIfNeeded(ctx: ExtensionContext) {
354
359
  if (storeUpgraded) return;
@@ -378,6 +383,7 @@ export default function (pi: ExtensionAPI) {
378
383
 
379
384
  pi.on("turn_start", async (_event, ctx) => {
380
385
  _latestCtx = ctx;
386
+ _sessionId = ctx.sessionManager.getSessionId();
381
387
  widget.setUICtx(ctx.ui);
382
388
  upgradeStoreIfNeeded(ctx);
383
389
  widget.update();
@@ -417,6 +423,7 @@ export default function (pi: ExtensionAPI) {
417
423
  triggerSystem.stop();
418
424
  agentRunning = false;
419
425
  pendingNotifications.clear();
426
+ _sessionId = undefined;
420
427
 
421
428
  const isResume = event?.reason === "resume";
422
429
  storeUpgraded = false;
@@ -998,6 +1005,39 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
998
1005
  ui.notify(`${active}/${loops.length} active loops (max 25)`, "info");
999
1006
  }
1000
1007
 
1008
+ const AUTO_TASK_WORKER_THRESHOLD = 5;
1009
+ 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.";
1010
+
1011
+ function findAutoTaskWorkerLoop(): LoopEntry | undefined {
1012
+ return store.list().find(entry =>
1013
+ entry.status === "active"
1014
+ && entry.prompt === AUTO_TASK_WORKER_PROMPT
1015
+ && triggerHasEventSource(entry.trigger, "tasks:created")
1016
+ );
1017
+ }
1018
+
1019
+ async function ensureAutoTaskWorkerLoop(taskStore: TaskStore): Promise<{ entry?: LoopEntry; created: boolean }> {
1020
+ if (taskStore.pendingCount() < AUTO_TASK_WORKER_THRESHOLD) return { created: false };
1021
+
1022
+ const existing = findAutoTaskWorkerLoop();
1023
+ if (existing) return { entry: existing, created: false };
1024
+
1025
+ const trigger: Trigger = {
1026
+ type: "hybrid",
1027
+ cron: "*/5 * * * *",
1028
+ event: { source: "tasks:created" },
1029
+ debounceMs: 30000,
1030
+ };
1031
+ const entry = store.create(trigger, AUTO_TASK_WORKER_PROMPT, {
1032
+ recurring: true,
1033
+ maxFires: 30,
1034
+ });
1035
+ triggerSystem.add(entry);
1036
+ await maybeBootstrapTaskLoop(entry);
1037
+ widget.update();
1038
+ return { entry, created: true };
1039
+ }
1040
+
1001
1041
  async function createNativeTaskInteractively(ui: ExtensionUIContext) {
1002
1042
  if (!nativeTaskStore) {
1003
1043
  ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
@@ -1008,8 +1048,18 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
1008
1048
  if (!subject) return;
1009
1049
  const description = await ui.input("Task description") || subject;
1010
1050
  const entry = nativeTaskStore.create(subject, description);
1051
+ pi.events.emit("tasks:created", {
1052
+ taskId: entry.id,
1053
+ subject: entry.subject,
1054
+ description: entry.description,
1055
+ status: entry.status,
1056
+ });
1057
+ const worker = await ensureAutoTaskWorkerLoop(nativeTaskStore);
1011
1058
  widget.update();
1012
1059
  ui.notify(`Task #${entry.id} created`, "info");
1060
+ if (worker.created && worker.entry) {
1061
+ ui.notify(`Worker loop #${worker.entry.id} auto-created`, "info");
1062
+ }
1013
1063
  }
1014
1064
 
1015
1065
  async function viewNativeTasks(ui: ExtensionUIContext): Promise<void> {
@@ -1076,7 +1126,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
1076
1126
 
1077
1127
  setTimeout(async () => {
1078
1128
  if (tasksAvailable || nativeTasksRegistered) return;
1079
- nativeTaskStore = new TaskStore(resolveTaskStorePath());
1129
+ nativeTaskStore = new TaskStore(resolveTaskStorePath(_sessionId));
1080
1130
  nativeTasksRegistered = true;
1081
1131
  const taskStore = nativeTaskStore;
1082
1132
 
@@ -1096,8 +1146,12 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
1096
1146
  description: entry.description,
1097
1147
  status: entry.status,
1098
1148
  });
1149
+ const worker = await ensureAutoTaskWorkerLoop(nativeTaskStore);
1099
1150
  widget.update();
1100
1151
  ctx.ui.notify(`Task #${entry.id} created`, "info");
1152
+ if (worker.created && worker.entry) {
1153
+ ctx.ui.notify(`Worker loop #${worker.entry.id} auto-created`, "info");
1154
+ }
1101
1155
  return;
1102
1156
  }
1103
1157
  await viewNativeTasks(ctx.ui);
@@ -1115,13 +1169,14 @@ Fields:
1115
1169
  - metadata: optional tags/metadata`,
1116
1170
  promptGuidelines: [
1117
1171
  "Use TaskCreate to track complex multi-step work across turns.",
1172
+ "Break work into small, independently completable tasks. A task should be finishable in one focused session — if a task would take multiple turns, split it further.",
1118
1173
  "TaskCreate accepts `subject` and `description` parameters only — do not invent extra fields unless the schema explicitly adds them.",
1119
1174
  ],
1120
1175
  parameters: Type.Object({
1121
1176
  subject: Type.String({ description: "Brief actionable title for the task" }),
1122
1177
  description: Type.String({ description: "Detailed description of what needs to be done" }),
1123
1178
  }),
1124
- execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
1179
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
1125
1180
  const entry = taskStore.create(params.subject, params.description);
1126
1181
  pi.events.emit("tasks:created", {
1127
1182
  taskId: entry.id,
@@ -1129,13 +1184,13 @@ Fields:
1129
1184
  description: entry.description,
1130
1185
  status: entry.status,
1131
1186
  });
1187
+ const worker = await ensureAutoTaskWorkerLoop(taskStore);
1132
1188
  widget.update();
1133
1189
 
1134
- const pending = taskStore.pendingCount();
1135
- const hint = pending >= 5
1136
- ? `\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.')`
1190
+ const autoLoopMsg = worker.created && worker.entry
1191
+ ? `\nWorker loop #${worker.entry.id} auto-created`
1137
1192
  : "";
1138
- return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${hint}`));
1193
+ return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${autoLoopMsg}`));
1139
1194
  },
1140
1195
  });
1141
1196