@trevonistrevon/pi-loop 0.5.3 → 0.5.5
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/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-final.json +32 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +176 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/commands/index.html +131 -0
- package/coverage/src/commands/loop-command.ts.html +634 -0
- package/coverage/src/commands/tasks-command.ts.html +490 -0
- package/coverage/src/coordinator.ts.html +430 -0
- package/coverage/src/goal-coordinator.ts.html +259 -0
- package/coverage/src/goal-reducer.ts.html +982 -0
- package/coverage/src/goal-store.ts.html +742 -0
- package/coverage/src/goal-verifier.ts.html +808 -0
- package/coverage/src/index.html +386 -0
- package/coverage/src/index.ts.html +1054 -0
- package/coverage/src/loop-parse.ts.html +574 -0
- package/coverage/src/loop-reducer.ts.html +559 -0
- package/coverage/src/monitor-completion-coordinator.ts.html +157 -0
- package/coverage/src/monitor-manager.ts.html +865 -0
- package/coverage/src/monitor-reducer.ts.html +583 -0
- package/coverage/src/notification-reducer.ts.html +550 -0
- package/coverage/src/reducer-backed-store.ts.html +589 -0
- package/coverage/src/runtime/index.html +191 -0
- package/coverage/src/runtime/monitor-ondone-runtime.ts.html +310 -0
- package/coverage/src/runtime/notification-runtime.ts.html +721 -0
- package/coverage/src/runtime/scope.ts.html +196 -0
- package/coverage/src/runtime/session-runtime.ts.html +589 -0
- package/coverage/src/runtime/task-backlog-runtime.ts.html +574 -0
- package/coverage/src/runtime/task-rpc.ts.html +535 -0
- package/coverage/src/scheduler.ts.html +400 -0
- package/coverage/src/store.ts.html +667 -0
- package/coverage/src/task-backlog-coordinator.ts.html +181 -0
- package/coverage/src/task-reducer.ts.html +550 -0
- package/coverage/src/task-store.ts.html +526 -0
- package/coverage/src/tools/index.html +146 -0
- package/coverage/src/tools/loop-tools.ts.html +1000 -0
- package/coverage/src/tools/monitor-tools.ts.html +547 -0
- package/coverage/src/tools/native-task-tools.ts.html +535 -0
- package/coverage/src/trigger-system.ts.html +547 -0
- package/coverage/src/ui/index.html +116 -0
- package/coverage/src/ui/widget.ts.html +292 -0
- package/dist/commands/loop-command.js +4 -5
- package/dist/commands/tasks-command.js +3 -3
- package/dist/goal-reducer.d.ts +9 -0
- package/dist/goal-reducer.js +11 -2
- package/dist/goal-store.d.ts +4 -15
- package/dist/goal-store.js +32 -154
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -3
- package/dist/loop-reducer.d.ts +7 -0
- package/dist/loop-reducer.js +9 -0
- package/dist/monitor-manager.js +4 -4
- package/dist/reducer-backed-store.d.ts +65 -0
- package/dist/reducer-backed-store.js +160 -0
- package/dist/store.d.ts +4 -16
- package/dist/store.js +25 -156
- package/dist/task-reducer.js +3 -0
- package/dist/task-store.d.ts +4 -15
- package/dist/task-store.js +25 -147
- package/dist/tools/loop-tools.js +7 -5
- package/dist/tools/monitor-tools.js +12 -3
- package/dist/tools/native-task-tools.js +2 -2
- package/dist/trigger-system.js +2 -1
- package/dist/ui/widget.js +8 -1
- package/package.json +4 -1
- package/src/commands/loop-command.ts +4 -5
- package/src/commands/tasks-command.ts +3 -3
- package/src/goal-reducer.ts +20 -1
- package/src/goal-store.ts +35 -142
- package/src/index.ts +4 -3
- package/src/loop-reducer.ts +10 -0
- package/src/monitor-manager.ts +2 -3
- package/src/reducer-backed-store.ts +168 -0
- package/src/store.ts +28 -141
- package/src/task-reducer.ts +3 -0
- package/src/task-store.ts +28 -135
- package/src/tools/loop-tools.ts +6 -5
- package/src/tools/monitor-tools.ts +12 -3
- package/src/tools/native-task-tools.ts +2 -2
- package/src/trigger-system.ts +2 -1
- package/src/ui/widget.ts +8 -1
- package/vitest.config.ts +25 -0
- package/DIFFERENTIAL_REVIEW_REPORT.md +0 -81
package/dist/store.js
CHANGED
|
@@ -1,135 +1,23 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
1
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
2
|
+
import { join } from "node:path";
|
|
4
3
|
import { reduceLoopState } from "./loop-reducer.js";
|
|
4
|
+
import { ReducerBackedStore } from "./reducer-backed-store.js";
|
|
5
5
|
const LOOPS_DIR = join(homedir(), ".pi", "loops");
|
|
6
|
-
const LOCK_RETRY_MS = 50;
|
|
7
|
-
const LOCK_MAX_RETRIES = 100;
|
|
8
6
|
const MAX_LOOPS = 25;
|
|
9
|
-
|
|
10
|
-
for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
|
|
11
|
-
try {
|
|
12
|
-
writeFileSync(lockPath, `${process.pid}`, { flag: "wx" });
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
catch (e) {
|
|
16
|
-
if (e.code === "EEXIST") {
|
|
17
|
-
try {
|
|
18
|
-
const pid = parseInt(readFileSync(lockPath, "utf-8"), 10);
|
|
19
|
-
if (!pid || !isProcessRunning(pid)) {
|
|
20
|
-
try {
|
|
21
|
-
unlinkSync(lockPath);
|
|
22
|
-
}
|
|
23
|
-
catch { /* ignore */ }
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
catch { /* ignore read errors */ }
|
|
28
|
-
const start = Date.now();
|
|
29
|
-
while (Date.now() - start < LOCK_RETRY_MS) { /* busy wait */ }
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
throw e;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
throw new Error(`Failed to acquire lock: ${lockPath}`);
|
|
36
|
-
}
|
|
37
|
-
function releaseLock(lockPath) {
|
|
38
|
-
try {
|
|
39
|
-
unlinkSync(lockPath);
|
|
40
|
-
}
|
|
41
|
-
catch { /* ignore */ }
|
|
42
|
-
}
|
|
43
|
-
function isProcessRunning(pid) {
|
|
44
|
-
try {
|
|
45
|
-
process.kill(pid, 0);
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
export class LoopStore {
|
|
53
|
-
filePath;
|
|
54
|
-
lockPath;
|
|
55
|
-
lastLoadedSignature;
|
|
56
|
-
nextId = 1;
|
|
57
|
-
loops = new Map();
|
|
7
|
+
export class LoopStore extends ReducerBackedStore {
|
|
58
8
|
constructor(listIdOrPath) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
getFileSignature() {
|
|
69
|
-
if (!this.filePath || !existsSync(this.filePath))
|
|
70
|
-
return undefined;
|
|
71
|
-
const stat = statSync(this.filePath);
|
|
72
|
-
return `${stat.mtimeMs}:${stat.size}`;
|
|
73
|
-
}
|
|
74
|
-
load(force = false) {
|
|
75
|
-
if (!this.filePath)
|
|
76
|
-
return;
|
|
77
|
-
const signature = this.getFileSignature();
|
|
78
|
-
if (!signature)
|
|
79
|
-
return;
|
|
80
|
-
if (!force && signature === this.lastLoadedSignature)
|
|
81
|
-
return;
|
|
82
|
-
try {
|
|
83
|
-
const data = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
84
|
-
this.nextId = data.nextId;
|
|
85
|
-
this.loops.clear();
|
|
86
|
-
for (const loop of data.loops) {
|
|
87
|
-
this.loops.set(loop.id, loop);
|
|
88
|
-
}
|
|
89
|
-
this.lastLoadedSignature = signature;
|
|
90
|
-
}
|
|
91
|
-
catch { /* corrupt file — start fresh */ }
|
|
92
|
-
}
|
|
93
|
-
save() {
|
|
94
|
-
if (!this.filePath)
|
|
95
|
-
return;
|
|
96
|
-
const data = {
|
|
97
|
-
nextId: this.nextId,
|
|
98
|
-
loops: Array.from(this.loops.values()),
|
|
99
|
-
};
|
|
100
|
-
const tmpPath = this.filePath + ".tmp";
|
|
101
|
-
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
102
|
-
renameSync(tmpPath, this.filePath);
|
|
103
|
-
this.lastLoadedSignature = this.getFileSignature();
|
|
104
|
-
}
|
|
105
|
-
withLock(fn) {
|
|
106
|
-
if (!this.lockPath)
|
|
107
|
-
return fn();
|
|
108
|
-
acquireLock(this.lockPath);
|
|
109
|
-
try {
|
|
110
|
-
this.load(true);
|
|
111
|
-
const result = fn();
|
|
112
|
-
this.save();
|
|
113
|
-
return result;
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
releaseLock(this.lockPath);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
toReducerState() {
|
|
120
|
-
return {
|
|
121
|
-
nextId: this.nextId,
|
|
122
|
-
loopsById: Object.fromEntries(this.loops.entries()),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
applyReducerEvent(event) {
|
|
126
|
-
const result = reduceLoopState(this.toReducerState(), event);
|
|
127
|
-
this.nextId = result.state.nextId;
|
|
128
|
-
this.loops = new Map(Object.entries(result.state.loopsById));
|
|
9
|
+
super({
|
|
10
|
+
baseDir: LOOPS_DIR,
|
|
11
|
+
reduce: (state, event) => reduceLoopState(state, event),
|
|
12
|
+
toReducerState: (nextId, entries) => ({ nextId, loopsById: Object.fromEntries(entries.entries()) }),
|
|
13
|
+
fromReducerState: (state) => ({ nextId: state.nextId, entries: new Map(Object.entries(state.loopsById)) }),
|
|
14
|
+
serialize: (nextId, entries) => ({ nextId, loops: Array.from(entries.values()) }),
|
|
15
|
+
deserialize: (data) => ({ nextId: data.nextId, entries: new Map(data.loops.map((l) => [l.id, l])) }),
|
|
16
|
+
}, listIdOrPath);
|
|
129
17
|
}
|
|
130
18
|
create(trigger, prompt, opts) {
|
|
131
19
|
return this.withLock(() => {
|
|
132
|
-
if (this.
|
|
20
|
+
if (this.entries.size >= MAX_LOOPS) {
|
|
133
21
|
throw new Error(`Maximum of ${MAX_LOOPS} loops reached. Delete some before creating new ones.`);
|
|
134
22
|
}
|
|
135
23
|
const now = Date.now();
|
|
@@ -148,22 +36,12 @@ export class LoopStore {
|
|
|
148
36
|
maxFires: opts.maxFires,
|
|
149
37
|
},
|
|
150
38
|
});
|
|
151
|
-
return this.
|
|
39
|
+
return this.entries.get(String(this.nextId - 1));
|
|
152
40
|
});
|
|
153
41
|
}
|
|
154
|
-
get(id) {
|
|
155
|
-
if (this.filePath)
|
|
156
|
-
this.load();
|
|
157
|
-
return this.loops.get(id);
|
|
158
|
-
}
|
|
159
|
-
list() {
|
|
160
|
-
if (this.filePath)
|
|
161
|
-
this.load();
|
|
162
|
-
return Array.from(this.loops.values()).sort((a, b) => Number(a.id) - Number(b.id));
|
|
163
|
-
}
|
|
164
42
|
pause(id) {
|
|
165
43
|
return this.withLock(() => {
|
|
166
|
-
const entry = this.
|
|
44
|
+
const entry = this.entries.get(id);
|
|
167
45
|
if (!entry)
|
|
168
46
|
return undefined;
|
|
169
47
|
this.applyReducerEvent({
|
|
@@ -174,12 +52,12 @@ export class LoopStore {
|
|
|
174
52
|
entityId: id,
|
|
175
53
|
payload: { id },
|
|
176
54
|
});
|
|
177
|
-
return this.
|
|
55
|
+
return this.entries.get(id);
|
|
178
56
|
});
|
|
179
57
|
}
|
|
180
58
|
resume(id) {
|
|
181
59
|
return this.withLock(() => {
|
|
182
|
-
const entry = this.
|
|
60
|
+
const entry = this.entries.get(id);
|
|
183
61
|
if (!entry)
|
|
184
62
|
return undefined;
|
|
185
63
|
this.applyReducerEvent({
|
|
@@ -190,12 +68,12 @@ export class LoopStore {
|
|
|
190
68
|
entityId: id,
|
|
191
69
|
payload: { id },
|
|
192
70
|
});
|
|
193
|
-
return this.
|
|
71
|
+
return this.entries.get(id);
|
|
194
72
|
});
|
|
195
73
|
}
|
|
196
74
|
fire(id) {
|
|
197
75
|
return this.withLock(() => {
|
|
198
|
-
const entry = this.
|
|
76
|
+
const entry = this.entries.get(id);
|
|
199
77
|
if (!entry)
|
|
200
78
|
return undefined;
|
|
201
79
|
this.applyReducerEvent({
|
|
@@ -206,12 +84,12 @@ export class LoopStore {
|
|
|
206
84
|
entityId: id,
|
|
207
85
|
payload: { id },
|
|
208
86
|
});
|
|
209
|
-
return this.
|
|
87
|
+
return this.entries.get(id);
|
|
210
88
|
});
|
|
211
89
|
}
|
|
212
90
|
updateMetadata(id, fields) {
|
|
213
91
|
return this.withLock(() => {
|
|
214
|
-
const current = this.
|
|
92
|
+
const current = this.entries.get(id);
|
|
215
93
|
if (!current)
|
|
216
94
|
return { entry: undefined, changedFields: [] };
|
|
217
95
|
const changedFields = [];
|
|
@@ -227,12 +105,12 @@ export class LoopStore {
|
|
|
227
105
|
if (changedFields.length > 0) {
|
|
228
106
|
current.updatedAt = now;
|
|
229
107
|
}
|
|
230
|
-
return { entry: this.
|
|
108
|
+
return { entry: this.entries.get(id), changedFields };
|
|
231
109
|
});
|
|
232
110
|
}
|
|
233
111
|
delete(id) {
|
|
234
112
|
return this.withLock(() => {
|
|
235
|
-
if (!this.
|
|
113
|
+
if (!this.entries.has(id))
|
|
236
114
|
return false;
|
|
237
115
|
this.applyReducerEvent({
|
|
238
116
|
type: "LOOP_DELETED",
|
|
@@ -249,7 +127,7 @@ export class LoopStore {
|
|
|
249
127
|
return this.withLock(() => {
|
|
250
128
|
const now = Date.now();
|
|
251
129
|
let count = 0;
|
|
252
|
-
for (const [id, entry] of [...this.
|
|
130
|
+
for (const [id, entry] of [...this.entries.entries()]) {
|
|
253
131
|
if (now < entry.expiresAt)
|
|
254
132
|
continue;
|
|
255
133
|
this.applyReducerEvent({
|
|
@@ -268,7 +146,7 @@ export class LoopStore {
|
|
|
268
146
|
expireEventLoops(sessionStartedAt) {
|
|
269
147
|
return this.withLock(() => {
|
|
270
148
|
let count = 0;
|
|
271
|
-
for (const [id, entry] of [...this.
|
|
149
|
+
for (const [id, entry] of [...this.entries.entries()]) {
|
|
272
150
|
if (entry.status !== "active")
|
|
273
151
|
continue;
|
|
274
152
|
if (entry.trigger.type !== "event" && entry.trigger.type !== "hybrid")
|
|
@@ -290,7 +168,7 @@ export class LoopStore {
|
|
|
290
168
|
}
|
|
291
169
|
clearAll() {
|
|
292
170
|
return this.withLock(() => {
|
|
293
|
-
const ids = [...this.
|
|
171
|
+
const ids = [...this.entries.keys()];
|
|
294
172
|
for (const id of ids) {
|
|
295
173
|
this.applyReducerEvent({
|
|
296
174
|
type: "LOOP_DELETED",
|
|
@@ -304,13 +182,4 @@ export class LoopStore {
|
|
|
304
182
|
return ids.length;
|
|
305
183
|
});
|
|
306
184
|
}
|
|
307
|
-
deleteFileIfEmpty() {
|
|
308
|
-
if (!this.filePath || this.loops.size > 0)
|
|
309
|
-
return false;
|
|
310
|
-
try {
|
|
311
|
-
unlinkSync(this.filePath);
|
|
312
|
-
}
|
|
313
|
-
catch { /* ignore */ }
|
|
314
|
-
return true;
|
|
315
|
-
}
|
|
316
185
|
}
|
package/dist/task-reducer.js
CHANGED
|
@@ -60,6 +60,9 @@ export function reduceTaskState(state, event) {
|
|
|
60
60
|
if (event.type === "TASK_REOPENED") {
|
|
61
61
|
task.status = "pending";
|
|
62
62
|
task.updatedAt = event.at;
|
|
63
|
+
// `completedAt` is intentionally retained: it records the most recent
|
|
64
|
+
// completion, not "is currently complete" (use `status` for that). A
|
|
65
|
+
// reopened task keeps the timestamp of when it was last completed.
|
|
63
66
|
}
|
|
64
67
|
if (event.type === "TASK_UPDATED") {
|
|
65
68
|
if (event.payload.subject !== undefined)
|
package/dist/task-store.d.ts
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
private lastLoadedSignature;
|
|
6
|
-
private nextId;
|
|
7
|
-
private tasks;
|
|
1
|
+
import { ReducerBackedStore } from "./reducer-backed-store.js";
|
|
2
|
+
import { type TaskReducerEvent, type TaskReducerState } from "./task-reducer.js";
|
|
3
|
+
import type { TaskEntry, TaskStoreData } from "./task-types.js";
|
|
4
|
+
export declare class TaskStore extends ReducerBackedStore<TaskEntry, TaskReducerState, TaskReducerEvent, TaskStoreData> {
|
|
8
5
|
constructor(listIdOrPath?: string);
|
|
9
|
-
private getFileSignature;
|
|
10
|
-
private load;
|
|
11
|
-
private save;
|
|
12
|
-
private withLock;
|
|
13
|
-
private toReducerState;
|
|
14
|
-
private applyReducerEvent;
|
|
15
6
|
create(subject: string, description: string, metadata?: Record<string, unknown>): TaskEntry;
|
|
16
|
-
get(id: string): TaskEntry | undefined;
|
|
17
|
-
list(): TaskEntry[];
|
|
18
7
|
start(id: string): TaskEntry | undefined;
|
|
19
8
|
complete(id: string): TaskEntry | undefined;
|
|
20
9
|
reopen(id: string): TaskEntry | undefined;
|
package/dist/task-store.js
CHANGED
|
@@ -1,135 +1,23 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
1
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { ReducerBackedStore } from "./reducer-backed-store.js";
|
|
4
4
|
import { reduceTaskState } from "./task-reducer.js";
|
|
5
5
|
const TASKS_DIR = join(homedir(), ".pi", "tasks");
|
|
6
|
-
const LOCK_RETRY_MS = 50;
|
|
7
|
-
const LOCK_MAX_RETRIES = 100;
|
|
8
6
|
const MAX_TASKS = 200;
|
|
9
|
-
|
|
10
|
-
for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
|
|
11
|
-
try {
|
|
12
|
-
writeFileSync(lockPath, `${process.pid}`, { flag: "wx" });
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
catch (e) {
|
|
16
|
-
if (e.code === "EEXIST") {
|
|
17
|
-
try {
|
|
18
|
-
const pid = parseInt(readFileSync(lockPath, "utf-8"), 10);
|
|
19
|
-
if (!pid || !isProcessRunning(pid)) {
|
|
20
|
-
try {
|
|
21
|
-
unlinkSync(lockPath);
|
|
22
|
-
}
|
|
23
|
-
catch { /* ignore */ }
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
catch { /* ignore read errors */ }
|
|
28
|
-
const start = Date.now();
|
|
29
|
-
while (Date.now() - start < LOCK_RETRY_MS) { /* busy wait */ }
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
throw e;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
throw new Error(`Failed to acquire lock: ${lockPath}`);
|
|
36
|
-
}
|
|
37
|
-
function releaseLock(lockPath) {
|
|
38
|
-
try {
|
|
39
|
-
unlinkSync(lockPath);
|
|
40
|
-
}
|
|
41
|
-
catch { /* ignore */ }
|
|
42
|
-
}
|
|
43
|
-
function isProcessRunning(pid) {
|
|
44
|
-
try {
|
|
45
|
-
process.kill(pid, 0);
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
export class TaskStore {
|
|
53
|
-
filePath;
|
|
54
|
-
lockPath;
|
|
55
|
-
lastLoadedSignature;
|
|
56
|
-
nextId = 1;
|
|
57
|
-
tasks = new Map();
|
|
7
|
+
export class TaskStore extends ReducerBackedStore {
|
|
58
8
|
constructor(listIdOrPath) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
getFileSignature() {
|
|
69
|
-
if (!this.filePath || !existsSync(this.filePath))
|
|
70
|
-
return undefined;
|
|
71
|
-
const stat = statSync(this.filePath);
|
|
72
|
-
return `${stat.mtimeMs}:${stat.size}`;
|
|
73
|
-
}
|
|
74
|
-
load(force = false) {
|
|
75
|
-
if (!this.filePath)
|
|
76
|
-
return;
|
|
77
|
-
const signature = this.getFileSignature();
|
|
78
|
-
if (!signature)
|
|
79
|
-
return;
|
|
80
|
-
if (!force && signature === this.lastLoadedSignature)
|
|
81
|
-
return;
|
|
82
|
-
try {
|
|
83
|
-
const data = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
84
|
-
this.nextId = data.nextId;
|
|
85
|
-
this.tasks.clear();
|
|
86
|
-
for (const task of data.tasks) {
|
|
87
|
-
this.tasks.set(task.id, task);
|
|
88
|
-
}
|
|
89
|
-
this.lastLoadedSignature = signature;
|
|
90
|
-
}
|
|
91
|
-
catch { /* corrupt file — start fresh */ }
|
|
92
|
-
}
|
|
93
|
-
save() {
|
|
94
|
-
if (!this.filePath)
|
|
95
|
-
return;
|
|
96
|
-
const data = {
|
|
97
|
-
nextId: this.nextId,
|
|
98
|
-
tasks: Array.from(this.tasks.values()),
|
|
99
|
-
};
|
|
100
|
-
const tmpPath = this.filePath + ".tmp";
|
|
101
|
-
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
102
|
-
renameSync(tmpPath, this.filePath);
|
|
103
|
-
this.lastLoadedSignature = this.getFileSignature();
|
|
104
|
-
}
|
|
105
|
-
withLock(fn) {
|
|
106
|
-
if (!this.lockPath)
|
|
107
|
-
return fn();
|
|
108
|
-
acquireLock(this.lockPath);
|
|
109
|
-
try {
|
|
110
|
-
this.load(true);
|
|
111
|
-
const result = fn();
|
|
112
|
-
this.save();
|
|
113
|
-
return result;
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
releaseLock(this.lockPath);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
toReducerState() {
|
|
120
|
-
return {
|
|
121
|
-
nextId: this.nextId,
|
|
122
|
-
tasksById: Object.fromEntries(this.tasks.entries()),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
applyReducerEvent(event) {
|
|
126
|
-
const result = reduceTaskState(this.toReducerState(), event);
|
|
127
|
-
this.nextId = result.state.nextId;
|
|
128
|
-
this.tasks = new Map(Object.entries(result.state.tasksById));
|
|
9
|
+
super({
|
|
10
|
+
baseDir: TASKS_DIR,
|
|
11
|
+
reduce: (state, event) => reduceTaskState(state, event),
|
|
12
|
+
toReducerState: (nextId, entries) => ({ nextId, tasksById: Object.fromEntries(entries.entries()) }),
|
|
13
|
+
fromReducerState: (state) => ({ nextId: state.nextId, entries: new Map(Object.entries(state.tasksById)) }),
|
|
14
|
+
serialize: (nextId, entries) => ({ nextId, tasks: Array.from(entries.values()) }),
|
|
15
|
+
deserialize: (data) => ({ nextId: data.nextId, entries: new Map(data.tasks.map((t) => [t.id, t])) }),
|
|
16
|
+
}, listIdOrPath);
|
|
129
17
|
}
|
|
130
18
|
create(subject, description, metadata) {
|
|
131
19
|
return this.withLock(() => {
|
|
132
|
-
if (this.
|
|
20
|
+
if (this.entries.size >= MAX_TASKS) {
|
|
133
21
|
throw new Error(`Maximum of ${MAX_TASKS} tasks reached. Delete some before creating new ones.`);
|
|
134
22
|
}
|
|
135
23
|
const now = Date.now();
|
|
@@ -140,22 +28,12 @@ export class TaskStore {
|
|
|
140
28
|
entityType: "task",
|
|
141
29
|
payload: { subject, description, metadata },
|
|
142
30
|
});
|
|
143
|
-
return this.
|
|
31
|
+
return this.entries.get(String(this.nextId - 1));
|
|
144
32
|
});
|
|
145
33
|
}
|
|
146
|
-
get(id) {
|
|
147
|
-
if (this.filePath)
|
|
148
|
-
this.load();
|
|
149
|
-
return this.tasks.get(id);
|
|
150
|
-
}
|
|
151
|
-
list() {
|
|
152
|
-
if (this.filePath)
|
|
153
|
-
this.load();
|
|
154
|
-
return Array.from(this.tasks.values()).sort((a, b) => Number(a.id) - Number(b.id));
|
|
155
|
-
}
|
|
156
34
|
start(id) {
|
|
157
35
|
return this.withLock(() => {
|
|
158
|
-
const entry = this.
|
|
36
|
+
const entry = this.entries.get(id);
|
|
159
37
|
if (!entry)
|
|
160
38
|
return undefined;
|
|
161
39
|
this.applyReducerEvent({
|
|
@@ -166,12 +44,12 @@ export class TaskStore {
|
|
|
166
44
|
entityId: id,
|
|
167
45
|
payload: { id },
|
|
168
46
|
});
|
|
169
|
-
return this.
|
|
47
|
+
return this.entries.get(id);
|
|
170
48
|
});
|
|
171
49
|
}
|
|
172
50
|
complete(id) {
|
|
173
51
|
return this.withLock(() => {
|
|
174
|
-
const entry = this.
|
|
52
|
+
const entry = this.entries.get(id);
|
|
175
53
|
if (!entry)
|
|
176
54
|
return undefined;
|
|
177
55
|
this.applyReducerEvent({
|
|
@@ -182,12 +60,12 @@ export class TaskStore {
|
|
|
182
60
|
entityId: id,
|
|
183
61
|
payload: { id },
|
|
184
62
|
});
|
|
185
|
-
return this.
|
|
63
|
+
return this.entries.get(id);
|
|
186
64
|
});
|
|
187
65
|
}
|
|
188
66
|
reopen(id) {
|
|
189
67
|
return this.withLock(() => {
|
|
190
|
-
const entry = this.
|
|
68
|
+
const entry = this.entries.get(id);
|
|
191
69
|
if (!entry)
|
|
192
70
|
return undefined;
|
|
193
71
|
this.applyReducerEvent({
|
|
@@ -198,12 +76,12 @@ export class TaskStore {
|
|
|
198
76
|
entityId: id,
|
|
199
77
|
payload: { id },
|
|
200
78
|
});
|
|
201
|
-
return this.
|
|
79
|
+
return this.entries.get(id);
|
|
202
80
|
});
|
|
203
81
|
}
|
|
204
82
|
updateDetails(id, fields) {
|
|
205
83
|
return this.withLock(() => {
|
|
206
|
-
const entry = this.
|
|
84
|
+
const entry = this.entries.get(id);
|
|
207
85
|
if (!entry)
|
|
208
86
|
return undefined;
|
|
209
87
|
if (fields.subject === undefined && fields.description === undefined)
|
|
@@ -220,12 +98,12 @@ export class TaskStore {
|
|
|
220
98
|
description: fields.description,
|
|
221
99
|
},
|
|
222
100
|
});
|
|
223
|
-
return this.
|
|
101
|
+
return this.entries.get(id);
|
|
224
102
|
});
|
|
225
103
|
}
|
|
226
104
|
delete(id) {
|
|
227
105
|
return this.withLock(() => {
|
|
228
|
-
if (!this.
|
|
106
|
+
if (!this.entries.has(id))
|
|
229
107
|
return false;
|
|
230
108
|
this.applyReducerEvent({
|
|
231
109
|
type: "TASK_DELETED",
|
|
@@ -240,7 +118,7 @@ export class TaskStore {
|
|
|
240
118
|
}
|
|
241
119
|
pendingCount() {
|
|
242
120
|
let count = 0;
|
|
243
|
-
for (const t of this.
|
|
121
|
+
for (const t of this.entries.values()) {
|
|
244
122
|
if (t.status === "pending" || t.status === "in_progress")
|
|
245
123
|
count++;
|
|
246
124
|
}
|
|
@@ -248,7 +126,7 @@ export class TaskStore {
|
|
|
248
126
|
}
|
|
249
127
|
pruneCompleted() {
|
|
250
128
|
return this.withLock(() => {
|
|
251
|
-
const before = this.
|
|
129
|
+
const before = this.entries.size;
|
|
252
130
|
this.applyReducerEvent({
|
|
253
131
|
type: "TASKS_PRUNED",
|
|
254
132
|
at: Date.now(),
|
|
@@ -256,7 +134,7 @@ export class TaskStore {
|
|
|
256
134
|
entityType: "task",
|
|
257
135
|
payload: { reason: "manual" },
|
|
258
136
|
});
|
|
259
|
-
return before - this.
|
|
137
|
+
return before - this.entries.size;
|
|
260
138
|
});
|
|
261
139
|
}
|
|
262
140
|
}
|
package/dist/tools/loop-tools.js
CHANGED
|
@@ -96,7 +96,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
96
96
|
"## Task-driven workflows",
|
|
97
97
|
"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.",
|
|
98
98
|
"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.",
|
|
99
|
-
"Set taskBacklog: true for
|
|
99
|
+
"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.",
|
|
100
100
|
"When no tasks are pending, the loop should stop itself or skip the wake entirely — no tokens burned on empty polls.",
|
|
101
101
|
"After creating a loop, tell the user the loop ID so they can cancel it with LoopDelete.",
|
|
102
102
|
],
|
|
@@ -170,10 +170,10 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
|
|
|
170
170
|
return Promise.resolve(textResult(`Loop #${entry.id} created: ${entry.prompt.slice(0, 60)}\n` +
|
|
171
171
|
`Trigger: ${triggerDesc}\n` +
|
|
172
172
|
`Recurring: ${entry.recurring}\n` +
|
|
173
|
-
(entry.autoTask ? "Auto-task: enabled\n" : "") +
|
|
174
|
-
(entry.taskBacklog ? "
|
|
175
|
-
(bootstrapped ? "
|
|
176
|
-
(isTaskSystemReady() ? "" : "
|
|
173
|
+
(entry.autoTask ? "Auto-create task: enabled\n" : "") +
|
|
174
|
+
(entry.taskBacklog ? "Backlog worker: enabled\n" : "") +
|
|
175
|
+
(bootstrapped ? "Backlog: initial wake queued for existing pending tasks\n" : "") +
|
|
176
|
+
(isTaskSystemReady() ? "" : "Task system: not ready yet — autoTask may not fire until native fallback or pi-tasks becomes available\n") +
|
|
177
177
|
`ID: ${entry.id} (use LoopDelete to cancel)`));
|
|
178
178
|
},
|
|
179
179
|
});
|
|
@@ -205,6 +205,8 @@ Use this before creating new loops to avoid duplicates, or to find IDs for LoopD
|
|
|
205
205
|
}
|
|
206
206
|
if (entry.autoTask)
|
|
207
207
|
line += " [auto-task]";
|
|
208
|
+
if (entry.taskBacklog)
|
|
209
|
+
line += " [backlog-worker]";
|
|
208
210
|
lines.push(line);
|
|
209
211
|
}
|
|
210
212
|
return Promise.resolve(textResult(lines.join("\n")));
|
|
@@ -52,10 +52,19 @@ Pass onDone with a prompt and the monitor auto-creates a one-shot loop that fire
|
|
|
52
52
|
updateWidget();
|
|
53
53
|
let onDoneMsg = "";
|
|
54
54
|
if (params.onDone) {
|
|
55
|
+
// onDone delivery is callback-only: the loop below is delivered solely
|
|
56
|
+
// via MonitorManager.onComplete (see handleMonitorDoneLoop →
|
|
57
|
+
// monitor-ondone-runtime), so it fires exactly once by construction.
|
|
58
|
+
// The event-typed trigger is metadata, NOT a live subscription — this
|
|
59
|
+
// loop is deliberately NOT passed to triggerSystem.add(). The "event"
|
|
60
|
+
// type lets expireEventLoops() prune it if orphaned across a session
|
|
61
|
+
// and lets the widget render it as a monitor-completion wake. Do not
|
|
62
|
+
// triggerSystem.add() this loop, or the monitor:done event would fire it
|
|
63
|
+
// a second time.
|
|
55
64
|
const doneTrigger = { type: "event", source: "monitor:done", filter: JSON.stringify({ monitorId: entry.id }) };
|
|
56
65
|
const doneLoop = getStore().create(doneTrigger, params.onDone, { recurring: false });
|
|
57
66
|
handleMonitorDoneLoop(doneLoop, entry.id);
|
|
58
|
-
onDoneMsg = `\
|
|
67
|
+
onDoneMsg = `\nCompletion wake loop #${doneLoop.id}: fires when the monitor completes — no polling needed`;
|
|
59
68
|
}
|
|
60
69
|
return Promise.resolve(textResult(`Monitor #${entry.id} started: ${entry.command.slice(0, 60)}\n` +
|
|
61
70
|
`Output stream: monitor:output (monitorId: ${entry.id})\n` +
|
|
@@ -70,10 +79,10 @@ Pass onDone with a prompt and the monitor auto-creates a one-shot loop that fire
|
|
|
70
79
|
execute() {
|
|
71
80
|
const monitors = getMonitorManager().list();
|
|
72
81
|
if (monitors.length === 0)
|
|
73
|
-
return Promise.resolve(textResult("No monitors
|
|
82
|
+
return Promise.resolve(textResult("No monitors."));
|
|
74
83
|
const lines = [];
|
|
75
84
|
for (const m of monitors) {
|
|
76
|
-
const icon = m.status === "running" ? ">" : m.status === "completed" ? "ok" : "
|
|
85
|
+
const icon = m.status === "running" ? ">" : m.status === "completed" ? "ok" : "x";
|
|
77
86
|
const age = Date.now() - m.startedAt;
|
|
78
87
|
const ageStr = formatRemaining(age);
|
|
79
88
|
let line = `${icon} #${m.id} [${m.status}] ${m.command.slice(0, 60)} — ${m.outputLines} lines (${ageStr})`;
|
|
@@ -39,7 +39,7 @@ Fields:
|
|
|
39
39
|
const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
40
40
|
updateWidget();
|
|
41
41
|
const autoLoopMsg = backlog.created && backlog.entry
|
|
42
|
-
? `\
|
|
42
|
+
? `\nBacklog worker loop #${backlog.entry.id} created`
|
|
43
43
|
: "";
|
|
44
44
|
return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${autoLoopMsg}`));
|
|
45
45
|
},
|
|
@@ -108,7 +108,7 @@ Parameters: id (required), status, subject, description`,
|
|
|
108
108
|
const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
109
109
|
const statusMsg = status ? ` → ${status}` : "";
|
|
110
110
|
const autoLoopMsg = backlog.created && backlog.entry
|
|
111
|
-
? `\
|
|
111
|
+
? `\nBacklog worker loop #${backlog.entry.id} created`
|
|
112
112
|
: "";
|
|
113
113
|
return Promise.resolve(textResult(`Task #${id} updated${statusMsg}${autoLoopMsg}`));
|
|
114
114
|
},
|
package/dist/trigger-system.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { atMaxFires } from "./loop-reducer.js";
|
|
1
2
|
export class TriggerSystem {
|
|
2
3
|
pi;
|
|
3
4
|
scheduler;
|
|
@@ -103,7 +104,7 @@ export class TriggerSystem {
|
|
|
103
104
|
this.remove(entry.id);
|
|
104
105
|
return;
|
|
105
106
|
}
|
|
106
|
-
if (fresh.recurring &&
|
|
107
|
+
if (fresh.recurring && atMaxFires(fresh)) {
|
|
107
108
|
this.remove(fresh.id);
|
|
108
109
|
this.store.delete(fresh.id);
|
|
109
110
|
return;
|