@trevonistrevon/pi-loop 0.4.8 → 0.4.10
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 +57 -9
- package/dist/monitor-manager.d.ts +1 -0
- package/dist/monitor-manager.js +21 -0
- package/dist/store.d.ts +1 -0
- package/dist/store.js +1 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
- package/src/index.ts +60 -10
- package/src/monitor-manager.ts +19 -0
- package/src/store.ts +2 -1
- package/src/types.ts +2 -0
package/dist/index.js
CHANGED
|
@@ -183,6 +183,7 @@ export default function (pi) {
|
|
|
183
183
|
if (nativeTaskStore) {
|
|
184
184
|
nativeTaskStore.sweepCompleted();
|
|
185
185
|
widget.update();
|
|
186
|
+
await cleanupTaskBacklogLoops();
|
|
186
187
|
}
|
|
187
188
|
}
|
|
188
189
|
let agentRunning = false;
|
|
@@ -378,6 +379,7 @@ export default function (pi) {
|
|
|
378
379
|
_latestCtx = ctx;
|
|
379
380
|
widget.setUICtx(ctx.ui);
|
|
380
381
|
await flushPendingNotifications({ ignorePendingMessages: true });
|
|
382
|
+
await cleanupTaskBacklogLoops();
|
|
381
383
|
await pumpLoops();
|
|
382
384
|
});
|
|
383
385
|
pi.on("session_shutdown", async () => {
|
|
@@ -468,6 +470,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
468
470
|
- **prompt**: what to do when the loop fires (e.g., "check if the build passed")
|
|
469
471
|
- **recurring**: repeat or fire once (default: true)
|
|
470
472
|
- **autoTask**: when pi-tasks is loaded or native task fallback is active, auto-create a task on each fire
|
|
473
|
+
- **taskBacklog**: mark this as a task-backlog worker loop so it auto-deletes when pending tasks reach zero
|
|
471
474
|
- **readOnly**: restrict the agent to read-only tools when this loop fires (default: false)
|
|
472
475
|
- **maxFires**: auto-stop after N fires — prevents infinite token burn on polling loops`,
|
|
473
476
|
promptGuidelines: [
|
|
@@ -487,6 +490,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
487
490
|
"## Task-driven workflows",
|
|
488
491
|
"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.",
|
|
489
492
|
"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.",
|
|
493
|
+
"Set taskBacklog: true for task-worker loops that process the existing pending queue. Task-backlog loops bootstrap against existing pending tasks and auto-delete when the queue reaches zero.",
|
|
490
494
|
"When no tasks are pending, the loop should stop itself or skip the wake entirely — no tokens burned on empty polls.",
|
|
491
495
|
"After creating a loop, tell the user the loop ID so they can cancel it with LoopDelete.",
|
|
492
496
|
],
|
|
@@ -495,13 +499,14 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
495
499
|
prompt: Type.String({ description: "Prompt to run when the loop fires" }),
|
|
496
500
|
recurring: Type.Optional(Type.Boolean({ description: "Whether loop repeats (default: true)", default: true })),
|
|
497
501
|
autoTask: Type.Optional(Type.Boolean({ description: "Auto-create pi-tasks task on fire", default: false })),
|
|
502
|
+
taskBacklog: Type.Optional(Type.Boolean({ description: "Mark as a task-backlog worker loop that auto-deletes when pending tasks reach zero", default: false })),
|
|
498
503
|
triggerType: Type.Optional(Type.String({ description: "cron, event, or hybrid (inferred from trigger string if omitted)", enum: ["cron", "event", "hybrid"] })),
|
|
499
504
|
debounceMs: Type.Optional(Type.Number({ description: "Debounce for hybrid triggers (default: 30000)", default: 30000 })),
|
|
500
505
|
readOnly: Type.Optional(Type.Boolean({ description: "Restrict the agent to read-only tools when this loop fires (default: false)", default: false })),
|
|
501
506
|
maxFires: Type.Optional(Type.Number({ description: "Auto-stop after N fires. Prevents infinite token burn on polling loops." })),
|
|
502
507
|
}),
|
|
503
508
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
504
|
-
const { trigger: triggerInput, prompt, recurring, autoTask, triggerType, debounceMs, readOnly, maxFires } = params;
|
|
509
|
+
const { trigger: triggerInput, prompt, recurring, autoTask, taskBacklog, triggerType, debounceMs, readOnly, maxFires } = params;
|
|
505
510
|
let trigger;
|
|
506
511
|
const inferred = triggerType ?? inferTriggerType(triggerInput);
|
|
507
512
|
if (inferred === "cron") {
|
|
@@ -528,6 +533,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
528
533
|
const entry = store.create(trigger, prompt, {
|
|
529
534
|
recurring: recurring ?? (inferred !== "event"),
|
|
530
535
|
autoTask,
|
|
536
|
+
taskBacklog,
|
|
531
537
|
readOnly,
|
|
532
538
|
maxFires,
|
|
533
539
|
});
|
|
@@ -558,17 +564,31 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
558
564
|
`Trigger: ${triggerDesc}\n` +
|
|
559
565
|
`Recurring: ${entry.recurring}\n` +
|
|
560
566
|
(entry.autoTask ? `Auto-task: enabled\n` : "") +
|
|
567
|
+
(entry.taskBacklog ? `Task-backlog: enabled\n` : "") +
|
|
561
568
|
(bootstrapped ? "Bootstrap: queued initial wake for existing pending tasks\n" : "") +
|
|
562
569
|
((tasksAvailable || nativeTasksRegistered) ? "" : "(task system not ready yet — autoTask may not fire until native fallback or pi-tasks becomes available)\n") +
|
|
563
570
|
`ID: ${entry.id} (use LoopDelete to cancel)`));
|
|
564
571
|
},
|
|
565
572
|
});
|
|
566
573
|
function handleMonitorDoneLoop(doneLoop, monitorId) {
|
|
567
|
-
|
|
574
|
+
const deliver = () => {
|
|
575
|
+
const current = store.get(doneLoop.id);
|
|
576
|
+
if (!current)
|
|
577
|
+
return;
|
|
578
|
+
debug(`onDone loop #${doneLoop.id} — monitor #${monitorId} completed, delivering directly`);
|
|
579
|
+
onLoopFire(current);
|
|
580
|
+
store.delete(doneLoop.id);
|
|
581
|
+
};
|
|
582
|
+
const registered = monitorManager.onComplete(monitorId, deliver);
|
|
583
|
+
if (registered)
|
|
584
|
+
return;
|
|
568
585
|
const monitor = monitorManager.get(monitorId);
|
|
569
586
|
if (monitor && monitor.status !== "running") {
|
|
587
|
+
if (monitor.status === "completed") {
|
|
588
|
+
deliver();
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
570
591
|
debug(`onDone loop #${doneLoop.id} — monitor #${monitorId} already ${monitor.status}, expiring`);
|
|
571
|
-
triggerSystem.remove(doneLoop.id);
|
|
572
592
|
store.delete(doneLoop.id);
|
|
573
593
|
}
|
|
574
594
|
}
|
|
@@ -930,10 +950,33 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
930
950
|
}
|
|
931
951
|
const AUTO_TASK_WORKER_THRESHOLD = 5;
|
|
932
952
|
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
|
|
934
|
-
return
|
|
953
|
+
function isAutoTaskWorkerLoop(entry) {
|
|
954
|
+
return entry.status === "active"
|
|
935
955
|
&& entry.prompt === AUTO_TASK_WORKER_PROMPT
|
|
936
|
-
&& triggerHasEventSource(entry.trigger, "tasks:created")
|
|
956
|
+
&& triggerHasEventSource(entry.trigger, "tasks:created");
|
|
957
|
+
}
|
|
958
|
+
function isTaskBacklogLoop(entry) {
|
|
959
|
+
return entry.status === "active"
|
|
960
|
+
&& triggerHasEventSource(entry.trigger, "tasks:created")
|
|
961
|
+
&& (entry.taskBacklog === true || isAutoTaskWorkerLoop(entry));
|
|
962
|
+
}
|
|
963
|
+
function findAutoTaskWorkerLoop() {
|
|
964
|
+
return store.list().find(isAutoTaskWorkerLoop);
|
|
965
|
+
}
|
|
966
|
+
async function cleanupTaskBacklogLoops() {
|
|
967
|
+
const backlogLoops = store.list().filter(isTaskBacklogLoop);
|
|
968
|
+
if (backlogLoops.length === 0)
|
|
969
|
+
return 0;
|
|
970
|
+
const pending = await hasPendingTasks();
|
|
971
|
+
if (pending < 0 || pending > 0)
|
|
972
|
+
return 0;
|
|
973
|
+
for (const entry of backlogLoops) {
|
|
974
|
+
debug(`task backlog loop #${entry.id} — no pending tasks remain, deleting`);
|
|
975
|
+
triggerSystem.remove(entry.id);
|
|
976
|
+
store.delete(entry.id);
|
|
977
|
+
}
|
|
978
|
+
widget.update();
|
|
979
|
+
return backlogLoops.length;
|
|
937
980
|
}
|
|
938
981
|
async function ensureAutoTaskWorkerLoop(taskStore) {
|
|
939
982
|
if (taskStore.pendingCount() < AUTO_TASK_WORKER_THRESHOLD)
|
|
@@ -949,6 +992,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
949
992
|
};
|
|
950
993
|
const entry = store.create(trigger, AUTO_TASK_WORKER_PROMPT, {
|
|
951
994
|
recurring: true,
|
|
995
|
+
taskBacklog: true,
|
|
952
996
|
maxFires: 30,
|
|
953
997
|
});
|
|
954
998
|
triggerSystem.add(entry);
|
|
@@ -1037,6 +1081,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1037
1081
|
ui.notify(`Task #${task.id} reopened`, "info");
|
|
1038
1082
|
}
|
|
1039
1083
|
widget.update();
|
|
1084
|
+
await cleanupTaskBacklogLoops();
|
|
1040
1085
|
return viewNativeTasks(ui);
|
|
1041
1086
|
}
|
|
1042
1087
|
// ── Native task tools (only when pi-tasks is absent) ──
|
|
@@ -1149,7 +1194,7 @@ Parameters: id (required), status, subject, description`,
|
|
|
1149
1194
|
subject: Type.Optional(Type.String({ description: "New title" })),
|
|
1150
1195
|
description: Type.Optional(Type.String({ description: "New description" })),
|
|
1151
1196
|
}),
|
|
1152
|
-
execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1197
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1153
1198
|
const { id, status, subject, description } = params;
|
|
1154
1199
|
const entry = taskStore.update(id, {
|
|
1155
1200
|
status: status,
|
|
@@ -1159,6 +1204,7 @@ Parameters: id (required), status, subject, description`,
|
|
|
1159
1204
|
if (!entry)
|
|
1160
1205
|
return Promise.resolve(textResult(`Task #${id} not found`));
|
|
1161
1206
|
widget.update();
|
|
1207
|
+
await cleanupTaskBacklogLoops();
|
|
1162
1208
|
const statusMsg = status ? ` → ${status}` : "";
|
|
1163
1209
|
return Promise.resolve(textResult(`Task #${id} updated${statusMsg}`));
|
|
1164
1210
|
},
|
|
@@ -1170,11 +1216,13 @@ Parameters: id (required), status, subject, description`,
|
|
|
1170
1216
|
parameters: Type.Object({
|
|
1171
1217
|
id: Type.String({ description: "Task ID to delete" }),
|
|
1172
1218
|
}),
|
|
1173
|
-
execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1219
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1174
1220
|
const deleted = taskStore.delete(params.id);
|
|
1175
1221
|
widget.update();
|
|
1176
|
-
if (deleted)
|
|
1222
|
+
if (deleted) {
|
|
1223
|
+
await cleanupTaskBacklogLoops();
|
|
1177
1224
|
return Promise.resolve(textResult(`Task #${params.id} deleted`));
|
|
1225
|
+
}
|
|
1178
1226
|
return Promise.resolve(textResult(`Task #${params.id} not found`));
|
|
1179
1227
|
},
|
|
1180
1228
|
});
|
package/dist/monitor-manager.js
CHANGED
|
@@ -30,6 +30,7 @@ export class MonitorManager {
|
|
|
30
30
|
proc: child,
|
|
31
31
|
abortController,
|
|
32
32
|
waiters: [],
|
|
33
|
+
completionCallbacks: [],
|
|
33
34
|
};
|
|
34
35
|
child.stdout?.on("data", (data) => {
|
|
35
36
|
const lines = data.toString().split("\n");
|
|
@@ -70,6 +71,11 @@ export class MonitorManager {
|
|
|
70
71
|
exitCode: code,
|
|
71
72
|
outputLines: entry.outputLines,
|
|
72
73
|
});
|
|
74
|
+
if (status === "completed") {
|
|
75
|
+
for (const callback of bp.completionCallbacks)
|
|
76
|
+
callback();
|
|
77
|
+
}
|
|
78
|
+
bp.completionCallbacks = [];
|
|
73
79
|
for (const resolve of bp.waiters)
|
|
74
80
|
resolve();
|
|
75
81
|
bp.waiters = [];
|
|
@@ -90,6 +96,7 @@ export class MonitorManager {
|
|
|
90
96
|
monitorId: id,
|
|
91
97
|
error: err.message,
|
|
92
98
|
});
|
|
99
|
+
bp.completionCallbacks = [];
|
|
93
100
|
for (const resolve of bp.waiters)
|
|
94
101
|
resolve();
|
|
95
102
|
bp.waiters = [];
|
|
@@ -134,11 +141,25 @@ export class MonitorManager {
|
|
|
134
141
|
});
|
|
135
142
|
});
|
|
136
143
|
bp.entry.completedAt = Date.now();
|
|
144
|
+
bp.completionCallbacks = [];
|
|
137
145
|
for (const resolve of bp.waiters)
|
|
138
146
|
resolve();
|
|
139
147
|
bp.waiters = [];
|
|
140
148
|
return true;
|
|
141
149
|
}
|
|
150
|
+
onComplete(id, callback) {
|
|
151
|
+
const bp = this.processes.get(id);
|
|
152
|
+
if (!bp)
|
|
153
|
+
return false;
|
|
154
|
+
if (bp.entry.status === "completed") {
|
|
155
|
+
callback();
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
if (bp.entry.status !== "running")
|
|
159
|
+
return false;
|
|
160
|
+
bp.completionCallbacks.push(callback);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
142
163
|
getProcess(id) {
|
|
143
164
|
return this.processes.get(id);
|
|
144
165
|
}
|
package/dist/store.d.ts
CHANGED
package/dist/store.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export interface LoopEntry {
|
|
|
28
28
|
updatedAt: number;
|
|
29
29
|
expiresAt: number;
|
|
30
30
|
autoTask?: boolean;
|
|
31
|
+
taskBacklog?: boolean;
|
|
31
32
|
readOnly?: boolean;
|
|
32
33
|
maxFires?: number;
|
|
33
34
|
fireCount?: number;
|
|
@@ -54,4 +55,5 @@ export interface MonitorProcess {
|
|
|
54
55
|
proc: import("node:child_process").ChildProcess;
|
|
55
56
|
abortController: AbortController;
|
|
56
57
|
waiters: Array<() => void>;
|
|
58
|
+
completionCallbacks: Array<() => void>;
|
|
57
59
|
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -196,6 +196,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
196
196
|
if (nativeTaskStore) {
|
|
197
197
|
nativeTaskStore.sweepCompleted();
|
|
198
198
|
widget.update();
|
|
199
|
+
await cleanupTaskBacklogLoops();
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
|
|
@@ -410,6 +411,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
410
411
|
_latestCtx = ctx;
|
|
411
412
|
widget.setUICtx(ctx.ui);
|
|
412
413
|
await flushPendingNotifications({ ignorePendingMessages: true });
|
|
414
|
+
await cleanupTaskBacklogLoops();
|
|
413
415
|
await pumpLoops();
|
|
414
416
|
});
|
|
415
417
|
|
|
@@ -508,6 +510,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
508
510
|
- **prompt**: what to do when the loop fires (e.g., "check if the build passed")
|
|
509
511
|
- **recurring**: repeat or fire once (default: true)
|
|
510
512
|
- **autoTask**: when pi-tasks is loaded or native task fallback is active, auto-create a task on each fire
|
|
513
|
+
- **taskBacklog**: mark this as a task-backlog worker loop so it auto-deletes when pending tasks reach zero
|
|
511
514
|
- **readOnly**: restrict the agent to read-only tools when this loop fires (default: false)
|
|
512
515
|
- **maxFires**: auto-stop after N fires — prevents infinite token burn on polling loops`,
|
|
513
516
|
promptGuidelines: [
|
|
@@ -527,6 +530,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
527
530
|
"## Task-driven workflows",
|
|
528
531
|
"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.",
|
|
529
532
|
"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.",
|
|
533
|
+
"Set taskBacklog: true for task-worker loops that process the existing pending queue. Task-backlog loops bootstrap against existing pending tasks and auto-delete when the queue reaches zero.",
|
|
530
534
|
"When no tasks are pending, the loop should stop itself or skip the wake entirely — no tokens burned on empty polls.",
|
|
531
535
|
"After creating a loop, tell the user the loop ID so they can cancel it with LoopDelete.",
|
|
532
536
|
],
|
|
@@ -535,6 +539,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
535
539
|
prompt: Type.String({ description: "Prompt to run when the loop fires" }),
|
|
536
540
|
recurring: Type.Optional(Type.Boolean({ description: "Whether loop repeats (default: true)", default: true })),
|
|
537
541
|
autoTask: Type.Optional(Type.Boolean({ description: "Auto-create pi-tasks task on fire", default: false })),
|
|
542
|
+
taskBacklog: Type.Optional(Type.Boolean({ description: "Mark as a task-backlog worker loop that auto-deletes when pending tasks reach zero", default: false })),
|
|
538
543
|
triggerType: Type.Optional(Type.String({ description: "cron, event, or hybrid (inferred from trigger string if omitted)", enum: ["cron", "event", "hybrid"] })),
|
|
539
544
|
debounceMs: Type.Optional(Type.Number({ description: "Debounce for hybrid triggers (default: 30000)", default: 30000 })),
|
|
540
545
|
readOnly: Type.Optional(Type.Boolean({ description: "Restrict the agent to read-only tools when this loop fires (default: false)", default: false })),
|
|
@@ -542,7 +547,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
542
547
|
}),
|
|
543
548
|
|
|
544
549
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
545
|
-
const { trigger: triggerInput, prompt, recurring, autoTask, triggerType, debounceMs, readOnly, maxFires } = params;
|
|
550
|
+
const { trigger: triggerInput, prompt, recurring, autoTask, taskBacklog, triggerType, debounceMs, readOnly, maxFires } = params;
|
|
546
551
|
|
|
547
552
|
let trigger: Trigger;
|
|
548
553
|
const inferred = triggerType ?? inferTriggerType(triggerInput);
|
|
@@ -570,6 +575,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
570
575
|
const entry = store.create(trigger, prompt, {
|
|
571
576
|
recurring: recurring ?? (inferred !== "event"),
|
|
572
577
|
autoTask,
|
|
578
|
+
taskBacklog,
|
|
573
579
|
readOnly,
|
|
574
580
|
maxFires,
|
|
575
581
|
});
|
|
@@ -606,6 +612,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
606
612
|
`Trigger: ${triggerDesc}\n` +
|
|
607
613
|
`Recurring: ${entry.recurring}\n` +
|
|
608
614
|
(entry.autoTask ? `Auto-task: enabled\n` : "") +
|
|
615
|
+
(entry.taskBacklog ? `Task-backlog: enabled\n` : "") +
|
|
609
616
|
(bootstrapped ? "Bootstrap: queued initial wake for existing pending tasks\n" : "") +
|
|
610
617
|
((tasksAvailable || nativeTasksRegistered) ? "" : "(task system not ready yet — autoTask may not fire until native fallback or pi-tasks becomes available)\n") +
|
|
611
618
|
`ID: ${entry.id} (use LoopDelete to cancel)`
|
|
@@ -614,11 +621,24 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
614
621
|
});
|
|
615
622
|
|
|
616
623
|
function handleMonitorDoneLoop(doneLoop: LoopEntry, monitorId: string): void {
|
|
617
|
-
|
|
624
|
+
const deliver = () => {
|
|
625
|
+
const current = store.get(doneLoop.id);
|
|
626
|
+
if (!current) return;
|
|
627
|
+
debug(`onDone loop #${doneLoop.id} — monitor #${monitorId} completed, delivering directly`);
|
|
628
|
+
onLoopFire(current);
|
|
629
|
+
store.delete(doneLoop.id);
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const registered = monitorManager.onComplete(monitorId, deliver);
|
|
633
|
+
if (registered) return;
|
|
634
|
+
|
|
618
635
|
const monitor = monitorManager.get(monitorId);
|
|
619
636
|
if (monitor && monitor.status !== "running") {
|
|
637
|
+
if (monitor.status === "completed") {
|
|
638
|
+
deliver();
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
620
641
|
debug(`onDone loop #${doneLoop.id} — monitor #${monitorId} already ${monitor.status}, expiring`);
|
|
621
|
-
triggerSystem.remove(doneLoop.id);
|
|
622
642
|
store.delete(doneLoop.id);
|
|
623
643
|
}
|
|
624
644
|
}
|
|
@@ -1009,12 +1029,36 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1009
1029
|
const AUTO_TASK_WORKER_THRESHOLD = 5;
|
|
1010
1030
|
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
1031
|
|
|
1012
|
-
function
|
|
1013
|
-
return
|
|
1014
|
-
entry.status === "active"
|
|
1032
|
+
function isAutoTaskWorkerLoop(entry: LoopEntry): boolean {
|
|
1033
|
+
return entry.status === "active"
|
|
1015
1034
|
&& entry.prompt === AUTO_TASK_WORKER_PROMPT
|
|
1035
|
+
&& triggerHasEventSource(entry.trigger, "tasks:created");
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function isTaskBacklogLoop(entry: LoopEntry): boolean {
|
|
1039
|
+
return entry.status === "active"
|
|
1016
1040
|
&& triggerHasEventSource(entry.trigger, "tasks:created")
|
|
1017
|
-
|
|
1041
|
+
&& (entry.taskBacklog === true || isAutoTaskWorkerLoop(entry));
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function findAutoTaskWorkerLoop(): LoopEntry | undefined {
|
|
1045
|
+
return store.list().find(isAutoTaskWorkerLoop);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
async function cleanupTaskBacklogLoops(): Promise<number> {
|
|
1049
|
+
const backlogLoops = store.list().filter(isTaskBacklogLoop);
|
|
1050
|
+
if (backlogLoops.length === 0) return 0;
|
|
1051
|
+
|
|
1052
|
+
const pending = await hasPendingTasks();
|
|
1053
|
+
if (pending < 0 || pending > 0) return 0;
|
|
1054
|
+
|
|
1055
|
+
for (const entry of backlogLoops) {
|
|
1056
|
+
debug(`task backlog loop #${entry.id} — no pending tasks remain, deleting`);
|
|
1057
|
+
triggerSystem.remove(entry.id);
|
|
1058
|
+
store.delete(entry.id);
|
|
1059
|
+
}
|
|
1060
|
+
widget.update();
|
|
1061
|
+
return backlogLoops.length;
|
|
1018
1062
|
}
|
|
1019
1063
|
|
|
1020
1064
|
async function ensureAutoTaskWorkerLoop(taskStore: TaskStore): Promise<{ entry?: LoopEntry; created: boolean }> {
|
|
@@ -1031,6 +1075,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1031
1075
|
};
|
|
1032
1076
|
const entry = store.create(trigger, AUTO_TASK_WORKER_PROMPT, {
|
|
1033
1077
|
recurring: true,
|
|
1078
|
+
taskBacklog: true,
|
|
1034
1079
|
maxFires: 30,
|
|
1035
1080
|
});
|
|
1036
1081
|
triggerSystem.add(entry);
|
|
@@ -1120,6 +1165,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
|
|
|
1120
1165
|
}
|
|
1121
1166
|
|
|
1122
1167
|
widget.update();
|
|
1168
|
+
await cleanupTaskBacklogLoops();
|
|
1123
1169
|
return viewNativeTasks(ui);
|
|
1124
1170
|
}
|
|
1125
1171
|
|
|
@@ -1238,7 +1284,7 @@ Parameters: id (required), status, subject, description`,
|
|
|
1238
1284
|
subject: Type.Optional(Type.String({ description: "New title" })),
|
|
1239
1285
|
description: Type.Optional(Type.String({ description: "New description" })),
|
|
1240
1286
|
}),
|
|
1241
|
-
execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1287
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1242
1288
|
const { id, status, subject, description } = params;
|
|
1243
1289
|
const entry = taskStore.update(id, {
|
|
1244
1290
|
status: status as "pending" | "in_progress" | "completed" | undefined,
|
|
@@ -1247,6 +1293,7 @@ Parameters: id (required), status, subject, description`,
|
|
|
1247
1293
|
});
|
|
1248
1294
|
if (!entry) return Promise.resolve(textResult(`Task #${id} not found`));
|
|
1249
1295
|
widget.update();
|
|
1296
|
+
await cleanupTaskBacklogLoops();
|
|
1250
1297
|
const statusMsg = status ? ` → ${status}` : "";
|
|
1251
1298
|
return Promise.resolve(textResult(`Task #${id} updated${statusMsg}`));
|
|
1252
1299
|
},
|
|
@@ -1259,10 +1306,13 @@ Parameters: id (required), status, subject, description`,
|
|
|
1259
1306
|
parameters: Type.Object({
|
|
1260
1307
|
id: Type.String({ description: "Task ID to delete" }),
|
|
1261
1308
|
}),
|
|
1262
|
-
execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1309
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
1263
1310
|
const deleted = taskStore.delete(params.id);
|
|
1264
1311
|
widget.update();
|
|
1265
|
-
if (deleted)
|
|
1312
|
+
if (deleted) {
|
|
1313
|
+
await cleanupTaskBacklogLoops();
|
|
1314
|
+
return Promise.resolve(textResult(`Task #${params.id} deleted`));
|
|
1315
|
+
}
|
|
1266
1316
|
return Promise.resolve(textResult(`Task #${params.id} not found`));
|
|
1267
1317
|
},
|
|
1268
1318
|
});
|
package/src/monitor-manager.ts
CHANGED
|
@@ -35,6 +35,7 @@ export class MonitorManager {
|
|
|
35
35
|
proc: child,
|
|
36
36
|
abortController,
|
|
37
37
|
waiters: [],
|
|
38
|
+
completionCallbacks: [],
|
|
38
39
|
};
|
|
39
40
|
|
|
40
41
|
child.stdout?.on("data", (data: Buffer) => {
|
|
@@ -74,6 +75,10 @@ export class MonitorManager {
|
|
|
74
75
|
exitCode: code,
|
|
75
76
|
outputLines: entry.outputLines,
|
|
76
77
|
});
|
|
78
|
+
if (status === "completed") {
|
|
79
|
+
for (const callback of bp.completionCallbacks) callback();
|
|
80
|
+
}
|
|
81
|
+
bp.completionCallbacks = [];
|
|
77
82
|
for (const resolve of bp.waiters) resolve();
|
|
78
83
|
bp.waiters = [];
|
|
79
84
|
// Remove completed/errored monitors after a brief delay so tool
|
|
@@ -95,6 +100,7 @@ export class MonitorManager {
|
|
|
95
100
|
monitorId: id,
|
|
96
101
|
error: err.message,
|
|
97
102
|
});
|
|
103
|
+
bp.completionCallbacks = [];
|
|
98
104
|
for (const resolve of bp.waiters) resolve();
|
|
99
105
|
bp.waiters = [];
|
|
100
106
|
}
|
|
@@ -143,11 +149,24 @@ export class MonitorManager {
|
|
|
143
149
|
});
|
|
144
150
|
|
|
145
151
|
bp.entry.completedAt = Date.now();
|
|
152
|
+
bp.completionCallbacks = [];
|
|
146
153
|
for (const resolve of bp.waiters) resolve();
|
|
147
154
|
bp.waiters = [];
|
|
148
155
|
return true;
|
|
149
156
|
}
|
|
150
157
|
|
|
158
|
+
onComplete(id: string, callback: () => void): boolean {
|
|
159
|
+
const bp = this.processes.get(id);
|
|
160
|
+
if (!bp) return false;
|
|
161
|
+
if (bp.entry.status === "completed") {
|
|
162
|
+
callback();
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (bp.entry.status !== "running") return false;
|
|
166
|
+
bp.completionCallbacks.push(callback);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
151
170
|
getProcess(id: string): MonitorProcess | undefined {
|
|
152
171
|
return this.processes.get(id);
|
|
153
172
|
}
|
package/src/store.ts
CHANGED
|
@@ -95,7 +95,7 @@ export class LoopStore {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
create(trigger: Trigger, prompt: string, opts: { recurring: boolean; autoTask?: boolean; readOnly?: boolean; maxFires?: number }): LoopEntry {
|
|
98
|
+
create(trigger: Trigger, prompt: string, opts: { recurring: boolean; autoTask?: boolean; taskBacklog?: boolean; readOnly?: boolean; maxFires?: number }): LoopEntry {
|
|
99
99
|
return this.withLock(() => {
|
|
100
100
|
if (this.loops.size >= MAX_LOOPS) {
|
|
101
101
|
throw new Error(`Maximum of ${MAX_LOOPS} loops reached. Delete some before creating new ones.`);
|
|
@@ -108,6 +108,7 @@ export class LoopStore {
|
|
|
108
108
|
status: "active",
|
|
109
109
|
recurring: opts.recurring,
|
|
110
110
|
autoTask: opts.autoTask,
|
|
111
|
+
taskBacklog: opts.taskBacklog,
|
|
111
112
|
readOnly: opts.readOnly,
|
|
112
113
|
maxFires: opts.maxFires,
|
|
113
114
|
fireCount: 0,
|
package/src/types.ts
CHANGED
|
@@ -30,6 +30,7 @@ export interface LoopEntry {
|
|
|
30
30
|
updatedAt: number;
|
|
31
31
|
expiresAt: number;
|
|
32
32
|
autoTask?: boolean;
|
|
33
|
+
taskBacklog?: boolean;
|
|
33
34
|
readOnly?: boolean;
|
|
34
35
|
maxFires?: number;
|
|
35
36
|
fireCount?: number;
|
|
@@ -59,4 +60,5 @@ export interface MonitorProcess {
|
|
|
59
60
|
proc: import("node:child_process").ChildProcess;
|
|
60
61
|
abortController: AbortController;
|
|
61
62
|
waiters: Array<() => void>;
|
|
63
|
+
completionCallbacks: Array<() => void>;
|
|
62
64
|
}
|