@trevonistrevon/pi-loop 0.5.0 → 0.5.2
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 +11 -10
- package/dist/commands/loop-command.d.ts +22 -0
- package/dist/commands/loop-command.js +148 -0
- package/dist/commands/tasks-command.d.ts +15 -0
- package/dist/commands/tasks-command.js +117 -0
- package/dist/goal-coordinator.d.ts +22 -0
- package/dist/goal-coordinator.js +28 -0
- package/dist/goal-reducer.d.ts +89 -0
- package/dist/goal-reducer.js +181 -0
- package/dist/goal-store.d.ts +33 -0
- package/dist/goal-store.js +310 -0
- package/dist/goal-types.d.ts +4 -0
- package/dist/index.js +125 -1212
- package/dist/runtime/monitor-ondone-runtime.d.ts +13 -0
- package/dist/runtime/monitor-ondone-runtime.js +49 -0
- package/dist/runtime/notification-runtime.d.ts +34 -0
- package/dist/runtime/notification-runtime.js +152 -0
- package/dist/runtime/scope.d.ts +8 -0
- package/dist/runtime/scope.js +33 -0
- package/dist/runtime/session-runtime.d.ts +39 -0
- package/dist/runtime/session-runtime.js +110 -0
- package/dist/runtime/task-backlog-runtime.d.ts +36 -0
- package/dist/runtime/task-backlog-runtime.js +105 -0
- package/dist/runtime/task-rpc.d.ts +19 -0
- package/dist/runtime/task-rpc.js +118 -0
- package/dist/store.d.ts +7 -4
- package/dist/store.js +66 -48
- package/dist/task-store.d.ts +8 -4
- package/dist/task-store.js +80 -51
- package/dist/tools/loop-tools.d.ts +41 -0
- package/dist/tools/loop-tools.js +241 -0
- package/dist/tools/monitor-tools.d.ts +25 -0
- package/dist/tools/monitor-tools.js +110 -0
- package/dist/tools/native-task-tools.d.ts +15 -0
- package/dist/tools/native-task-tools.js +130 -0
- package/dist/ui/widget.d.ts +0 -1
- package/dist/ui/widget.js +1 -14
- package/package.json +1 -1
- package/src/commands/loop-command.ts +184 -0
- package/src/commands/tasks-command.ts +135 -0
- package/src/goal-coordinator.ts +58 -0
- package/src/goal-reducer.ts +280 -0
- package/src/goal-store.ts +326 -0
- package/src/goal-types.ts +5 -0
- package/src/index.ts +129 -1299
- package/src/runtime/monitor-ondone-runtime.ts +75 -0
- package/src/runtime/notification-runtime.ts +212 -0
- package/src/runtime/scope.ts +37 -0
- package/src/runtime/session-runtime.ts +168 -0
- package/src/runtime/task-backlog-runtime.ts +163 -0
- package/src/runtime/task-rpc.ts +150 -0
- package/src/store.ts +66 -49
- package/src/task-store.ts +76 -50
- package/src/tools/loop-tools.ts +304 -0
- package/src/tools/monitor-tools.ts +145 -0
- package/src/tools/native-task-tools.ts +147 -0
- package/src/ui/widget.ts +1 -16
- package/tmp/perf-cap-data/loops.json +455 -0
- package/tmp/perf-cap-data/tasks.json +1 -0
- package/tmp/perf-data/loops-0.json +1 -0
- package/tmp/perf-data/loops-10.json +1 -0
- package/tmp/perf-data/loops-100.json +1 -0
- package/tmp/perf-data/loops-1000.json +1 -0
- package/tmp/perf-data/loops-500.json +1 -0
- package/tmp/perf-data/tasks-0.json +1 -0
- package/tmp/perf-data/tasks-10.json +1 -0
- package/tmp/perf-data/tasks-100.json +1 -0
- package/tmp/perf-data/tasks-1000.json +1 -0
- package/tmp/perf-data/tasks-500.json +1 -0
- package/tmp/perf-data-session/loops-0.json +4 -0
- package/tmp/perf-data-session/loops-100.json +1805 -0
- package/tmp/perf-data-session/loops-1000.json +18005 -0
- package/tmp/perf-data-session/loops-25.json +455 -0
- package/tmp/perf-data-session/loops-500.json +9005 -0
- package/tmp/perf-data-session/tasks-0.json +1 -0
- package/tmp/perf-data-session/tasks-100.json +1 -0
- package/tmp/perf-data-session/tasks-1000.json +1 -0
- package/tmp/perf-data-session/tasks-25.json +1 -0
- package/tmp/perf-data-session/tasks-500.json +1 -0
- package/tmp/perf-session-init.js +98 -0
- package/tmp/perf-startup-audit.js +80 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export function createTaskRuntimeBridge(options) {
|
|
3
|
+
const { pi, isTasksAvailable, setTasksAvailable, getNativeTaskStore, onNativeTaskCreated, onNativeTasksPruned, debug, } = options;
|
|
4
|
+
function checkTasksVersion() {
|
|
5
|
+
const requestId = randomUUID();
|
|
6
|
+
const timer = setTimeout(() => {
|
|
7
|
+
unsub();
|
|
8
|
+
}, 5000);
|
|
9
|
+
const unsub = pi.events.on(`tasks:rpc:ping:reply:${requestId}`, (raw) => {
|
|
10
|
+
unsub();
|
|
11
|
+
clearTimeout(timer);
|
|
12
|
+
const remoteVersion = raw?.data?.version;
|
|
13
|
+
if (remoteVersion !== undefined)
|
|
14
|
+
setTasksAvailable(true);
|
|
15
|
+
});
|
|
16
|
+
pi.events.emit("tasks:rpc:ping", { requestId });
|
|
17
|
+
}
|
|
18
|
+
async function autoCreateTask(entry) {
|
|
19
|
+
if (!entry.autoTask)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (isTasksAvailable()) {
|
|
22
|
+
try {
|
|
23
|
+
const requestId = randomUUID();
|
|
24
|
+
const taskId = await new Promise((resolve) => {
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
unsub();
|
|
27
|
+
resolve(undefined);
|
|
28
|
+
}, 5000);
|
|
29
|
+
const unsub = pi.events.on(`tasks:rpc:create:reply:${requestId}`, (raw) => {
|
|
30
|
+
unsub();
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
const reply = raw;
|
|
33
|
+
if (reply.success && reply.data)
|
|
34
|
+
resolve(reply.data.id);
|
|
35
|
+
else
|
|
36
|
+
resolve(undefined);
|
|
37
|
+
});
|
|
38
|
+
pi.events.emit("tasks:rpc:create", {
|
|
39
|
+
requestId,
|
|
40
|
+
subject: entry.prompt.slice(0, 80),
|
|
41
|
+
description: `Auto-created from loop #${entry.id}`,
|
|
42
|
+
metadata: { loopId: entry.id, trigger: entry.trigger },
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
return taskId;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const nativeTaskStore = getNativeTaskStore();
|
|
52
|
+
if (!nativeTaskStore)
|
|
53
|
+
return undefined;
|
|
54
|
+
const task = nativeTaskStore.create(entry.prompt.slice(0, 80), `Auto-created from loop #${entry.id}`, { loopId: entry.id, trigger: entry.trigger });
|
|
55
|
+
onNativeTaskCreated?.(nativeTaskStore);
|
|
56
|
+
return task.id;
|
|
57
|
+
}
|
|
58
|
+
async function hasPendingTasks() {
|
|
59
|
+
if (isTasksAvailable()) {
|
|
60
|
+
try {
|
|
61
|
+
const requestId = randomUUID();
|
|
62
|
+
const count = await new Promise((resolve) => {
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
unsub();
|
|
65
|
+
resolve(-1);
|
|
66
|
+
}, 3000);
|
|
67
|
+
const unsub = pi.events.on(`tasks:rpc:pending:reply:${requestId}`, (raw) => {
|
|
68
|
+
unsub();
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
const reply = raw;
|
|
71
|
+
resolve(reply.success && reply.data ? reply.data.pending : -1);
|
|
72
|
+
});
|
|
73
|
+
pi.events.emit("tasks:rpc:pending", { requestId });
|
|
74
|
+
});
|
|
75
|
+
return count;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return -1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return getNativeTaskStore()?.pendingCount() ?? -1;
|
|
82
|
+
}
|
|
83
|
+
async function cleanDoneTasks() {
|
|
84
|
+
if (isTasksAvailable()) {
|
|
85
|
+
try {
|
|
86
|
+
const requestId = randomUUID();
|
|
87
|
+
await new Promise((resolve) => {
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
unsub();
|
|
90
|
+
resolve();
|
|
91
|
+
}, 3000);
|
|
92
|
+
const unsub = pi.events.on(`tasks:rpc:clean:reply:${requestId}`, () => {
|
|
93
|
+
unsub();
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
debug?.("tasks:rpc:clean — done tasks swept");
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
pi.events.emit("tasks:rpc:clean", { requestId });
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// timeout or error, ignore
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const nativeTaskStore = getNativeTaskStore();
|
|
107
|
+
if (!nativeTaskStore)
|
|
108
|
+
return;
|
|
109
|
+
nativeTaskStore.pruneCompleted();
|
|
110
|
+
await onNativeTasksPruned?.(nativeTaskStore);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
checkTasksVersion,
|
|
114
|
+
autoCreateTask,
|
|
115
|
+
hasPendingTasks,
|
|
116
|
+
cleanDoneTasks,
|
|
117
|
+
};
|
|
118
|
+
}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { LoopEntry,
|
|
1
|
+
import type { LoopEntry, Trigger } from "./types.js";
|
|
2
2
|
export declare class LoopStore {
|
|
3
3
|
private filePath;
|
|
4
4
|
private lockPath;
|
|
5
|
+
private lastLoadedSignature;
|
|
5
6
|
private nextId;
|
|
6
7
|
private loops;
|
|
7
8
|
constructor(listIdOrPath?: string);
|
|
9
|
+
private getFileSignature;
|
|
8
10
|
private load;
|
|
9
11
|
private save;
|
|
10
12
|
private withLock;
|
|
@@ -19,11 +21,12 @@ export declare class LoopStore {
|
|
|
19
21
|
}): LoopEntry;
|
|
20
22
|
get(id: string): LoopEntry | undefined;
|
|
21
23
|
list(): LoopEntry[];
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
pause(id: string): LoopEntry | undefined;
|
|
25
|
+
resume(id: string): LoopEntry | undefined;
|
|
26
|
+
fire(id: string): LoopEntry | undefined;
|
|
27
|
+
updateMetadata(id: string, fields: {
|
|
24
28
|
trigger?: Trigger;
|
|
25
29
|
prompt?: string;
|
|
26
|
-
fireCount?: number;
|
|
27
30
|
}): {
|
|
28
31
|
entry: LoopEntry | undefined;
|
|
29
32
|
changedFields: string[];
|
package/dist/store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { reduceLoopState } from "./loop-reducer.js";
|
|
@@ -52,6 +52,7 @@ function isProcessRunning(pid) {
|
|
|
52
52
|
export class LoopStore {
|
|
53
53
|
filePath;
|
|
54
54
|
lockPath;
|
|
55
|
+
lastLoadedSignature;
|
|
55
56
|
nextId = 1;
|
|
56
57
|
loops = new Map();
|
|
57
58
|
constructor(listIdOrPath) {
|
|
@@ -64,10 +65,19 @@ export class LoopStore {
|
|
|
64
65
|
this.lockPath = filePath + ".lock";
|
|
65
66
|
this.load();
|
|
66
67
|
}
|
|
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) {
|
|
68
75
|
if (!this.filePath)
|
|
69
76
|
return;
|
|
70
|
-
|
|
77
|
+
const signature = this.getFileSignature();
|
|
78
|
+
if (!signature)
|
|
79
|
+
return;
|
|
80
|
+
if (!force && signature === this.lastLoadedSignature)
|
|
71
81
|
return;
|
|
72
82
|
try {
|
|
73
83
|
const data = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
@@ -76,6 +86,7 @@ export class LoopStore {
|
|
|
76
86
|
for (const loop of data.loops) {
|
|
77
87
|
this.loops.set(loop.id, loop);
|
|
78
88
|
}
|
|
89
|
+
this.lastLoadedSignature = signature;
|
|
79
90
|
}
|
|
80
91
|
catch { /* corrupt file — start fresh */ }
|
|
81
92
|
}
|
|
@@ -89,13 +100,14 @@ export class LoopStore {
|
|
|
89
100
|
const tmpPath = this.filePath + ".tmp";
|
|
90
101
|
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
91
102
|
renameSync(tmpPath, this.filePath);
|
|
103
|
+
this.lastLoadedSignature = this.getFileSignature();
|
|
92
104
|
}
|
|
93
105
|
withLock(fn) {
|
|
94
106
|
if (!this.lockPath)
|
|
95
107
|
return fn();
|
|
96
108
|
acquireLock(this.lockPath);
|
|
97
109
|
try {
|
|
98
|
-
this.load();
|
|
110
|
+
this.load(true);
|
|
99
111
|
const result = fn();
|
|
100
112
|
this.save();
|
|
101
113
|
return result;
|
|
@@ -149,38 +161,61 @@ export class LoopStore {
|
|
|
149
161
|
this.load();
|
|
150
162
|
return Array.from(this.loops.values()).sort((a, b) => Number(a.id) - Number(b.id));
|
|
151
163
|
}
|
|
152
|
-
|
|
164
|
+
pause(id) {
|
|
165
|
+
return this.withLock(() => {
|
|
166
|
+
const entry = this.loops.get(id);
|
|
167
|
+
if (!entry)
|
|
168
|
+
return undefined;
|
|
169
|
+
this.applyReducerEvent({
|
|
170
|
+
type: "LOOP_PAUSED",
|
|
171
|
+
at: Date.now(),
|
|
172
|
+
source: "tool",
|
|
173
|
+
entityType: "loop",
|
|
174
|
+
entityId: id,
|
|
175
|
+
payload: { id },
|
|
176
|
+
});
|
|
177
|
+
return this.loops.get(id);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
resume(id) {
|
|
153
181
|
return this.withLock(() => {
|
|
154
182
|
const entry = this.loops.get(id);
|
|
155
183
|
if (!entry)
|
|
184
|
+
return undefined;
|
|
185
|
+
this.applyReducerEvent({
|
|
186
|
+
type: "LOOP_RESUMED",
|
|
187
|
+
at: Date.now(),
|
|
188
|
+
source: "tool",
|
|
189
|
+
entityType: "loop",
|
|
190
|
+
entityId: id,
|
|
191
|
+
payload: { id },
|
|
192
|
+
});
|
|
193
|
+
return this.loops.get(id);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
fire(id) {
|
|
197
|
+
return this.withLock(() => {
|
|
198
|
+
const entry = this.loops.get(id);
|
|
199
|
+
if (!entry)
|
|
200
|
+
return undefined;
|
|
201
|
+
this.applyReducerEvent({
|
|
202
|
+
type: "LOOP_FIRED",
|
|
203
|
+
at: Date.now(),
|
|
204
|
+
source: "system",
|
|
205
|
+
entityType: "loop",
|
|
206
|
+
entityId: id,
|
|
207
|
+
payload: { id },
|
|
208
|
+
});
|
|
209
|
+
return this.loops.get(id);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
updateMetadata(id, fields) {
|
|
213
|
+
return this.withLock(() => {
|
|
214
|
+
const current = this.loops.get(id);
|
|
215
|
+
if (!current)
|
|
156
216
|
return { entry: undefined, changedFields: [] };
|
|
157
217
|
const changedFields = [];
|
|
158
218
|
const now = Date.now();
|
|
159
|
-
if (fields.status === "paused") {
|
|
160
|
-
this.applyReducerEvent({
|
|
161
|
-
type: "LOOP_PAUSED",
|
|
162
|
-
at: now,
|
|
163
|
-
source: "tool",
|
|
164
|
-
entityType: "loop",
|
|
165
|
-
entityId: id,
|
|
166
|
-
payload: { id },
|
|
167
|
-
});
|
|
168
|
-
changedFields.push("status");
|
|
169
|
-
}
|
|
170
|
-
else if (fields.status === "active") {
|
|
171
|
-
this.applyReducerEvent({
|
|
172
|
-
type: "LOOP_RESUMED",
|
|
173
|
-
at: now,
|
|
174
|
-
source: "tool",
|
|
175
|
-
entityType: "loop",
|
|
176
|
-
entityId: id,
|
|
177
|
-
payload: { id },
|
|
178
|
-
});
|
|
179
|
-
changedFields.push("status");
|
|
180
|
-
}
|
|
181
|
-
const current = this.loops.get(id);
|
|
182
|
-
if (!current)
|
|
183
|
-
return { entry: undefined, changedFields };
|
|
184
219
|
if (fields.trigger !== undefined) {
|
|
185
220
|
current.trigger = fields.trigger;
|
|
186
221
|
changedFields.push("trigger");
|
|
@@ -189,24 +224,7 @@ export class LoopStore {
|
|
|
189
224
|
current.prompt = fields.prompt;
|
|
190
225
|
changedFields.push("prompt");
|
|
191
226
|
}
|
|
192
|
-
if (
|
|
193
|
-
if (fields.fireCount === (current.fireCount ?? 0) + 1) {
|
|
194
|
-
this.applyReducerEvent({
|
|
195
|
-
type: "LOOP_FIRED",
|
|
196
|
-
at: now,
|
|
197
|
-
source: "system",
|
|
198
|
-
entityType: "loop",
|
|
199
|
-
entityId: id,
|
|
200
|
-
payload: { id },
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
current.fireCount = fields.fireCount;
|
|
205
|
-
current.updatedAt = now;
|
|
206
|
-
}
|
|
207
|
-
changedFields.push("fireCount");
|
|
208
|
-
}
|
|
209
|
-
if (fields.trigger !== undefined || fields.prompt !== undefined) {
|
|
227
|
+
if (changedFields.length > 0) {
|
|
210
228
|
current.updatedAt = now;
|
|
211
229
|
}
|
|
212
230
|
return { entry: this.loops.get(id), changedFields };
|
package/dist/task-store.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import type { TaskEntry
|
|
1
|
+
import type { TaskEntry } from "./task-types.js";
|
|
2
2
|
export declare class TaskStore {
|
|
3
3
|
private filePath;
|
|
4
4
|
private lockPath;
|
|
5
|
+
private lastLoadedSignature;
|
|
5
6
|
private nextId;
|
|
6
7
|
private tasks;
|
|
7
8
|
constructor(listIdOrPath?: string);
|
|
9
|
+
private getFileSignature;
|
|
8
10
|
private load;
|
|
9
11
|
private save;
|
|
10
12
|
private withLock;
|
|
@@ -13,12 +15,14 @@ export declare class TaskStore {
|
|
|
13
15
|
create(subject: string, description: string, metadata?: Record<string, unknown>): TaskEntry;
|
|
14
16
|
get(id: string): TaskEntry | undefined;
|
|
15
17
|
list(): TaskEntry[];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
start(id: string): TaskEntry | undefined;
|
|
19
|
+
complete(id: string): TaskEntry | undefined;
|
|
20
|
+
reopen(id: string): TaskEntry | undefined;
|
|
21
|
+
updateDetails(id: string, fields: {
|
|
18
22
|
subject?: string;
|
|
19
23
|
description?: string;
|
|
20
24
|
}): TaskEntry | undefined;
|
|
21
25
|
delete(id: string): boolean;
|
|
22
26
|
pendingCount(): number;
|
|
23
|
-
|
|
27
|
+
pruneCompleted(): number;
|
|
24
28
|
}
|
package/dist/task-store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { reduceTaskState } from "./task-reducer.js";
|
|
@@ -52,6 +52,7 @@ function isProcessRunning(pid) {
|
|
|
52
52
|
export class TaskStore {
|
|
53
53
|
filePath;
|
|
54
54
|
lockPath;
|
|
55
|
+
lastLoadedSignature;
|
|
55
56
|
nextId = 1;
|
|
56
57
|
tasks = new Map();
|
|
57
58
|
constructor(listIdOrPath) {
|
|
@@ -64,10 +65,19 @@ export class TaskStore {
|
|
|
64
65
|
this.lockPath = filePath + ".lock";
|
|
65
66
|
this.load();
|
|
66
67
|
}
|
|
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) {
|
|
68
75
|
if (!this.filePath)
|
|
69
76
|
return;
|
|
70
|
-
|
|
77
|
+
const signature = this.getFileSignature();
|
|
78
|
+
if (!signature)
|
|
79
|
+
return;
|
|
80
|
+
if (!force && signature === this.lastLoadedSignature)
|
|
71
81
|
return;
|
|
72
82
|
try {
|
|
73
83
|
const data = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
@@ -76,6 +86,7 @@ export class TaskStore {
|
|
|
76
86
|
for (const task of data.tasks) {
|
|
77
87
|
this.tasks.set(task.id, task);
|
|
78
88
|
}
|
|
89
|
+
this.lastLoadedSignature = signature;
|
|
79
90
|
}
|
|
80
91
|
catch { /* corrupt file — start fresh */ }
|
|
81
92
|
}
|
|
@@ -89,13 +100,14 @@ export class TaskStore {
|
|
|
89
100
|
const tmpPath = this.filePath + ".tmp";
|
|
90
101
|
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
91
102
|
renameSync(tmpPath, this.filePath);
|
|
103
|
+
this.lastLoadedSignature = this.getFileSignature();
|
|
92
104
|
}
|
|
93
105
|
withLock(fn) {
|
|
94
106
|
if (!this.lockPath)
|
|
95
107
|
return fn();
|
|
96
108
|
acquireLock(this.lockPath);
|
|
97
109
|
try {
|
|
98
|
-
this.load();
|
|
110
|
+
this.load(true);
|
|
99
111
|
const result = fn();
|
|
100
112
|
this.save();
|
|
101
113
|
return result;
|
|
@@ -141,56 +153,73 @@ export class TaskStore {
|
|
|
141
153
|
this.load();
|
|
142
154
|
return Array.from(this.tasks.values()).sort((a, b) => Number(a.id) - Number(b.id));
|
|
143
155
|
}
|
|
144
|
-
|
|
156
|
+
start(id) {
|
|
145
157
|
return this.withLock(() => {
|
|
146
158
|
const entry = this.tasks.get(id);
|
|
147
159
|
if (!entry)
|
|
148
160
|
return undefined;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
161
|
+
this.applyReducerEvent({
|
|
162
|
+
type: "TASK_STARTED",
|
|
163
|
+
at: Date.now(),
|
|
164
|
+
source: "tool",
|
|
165
|
+
entityType: "task",
|
|
166
|
+
entityId: id,
|
|
167
|
+
payload: { id },
|
|
168
|
+
});
|
|
169
|
+
return this.tasks.get(id);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
complete(id) {
|
|
173
|
+
return this.withLock(() => {
|
|
174
|
+
const entry = this.tasks.get(id);
|
|
175
|
+
if (!entry)
|
|
176
|
+
return undefined;
|
|
177
|
+
this.applyReducerEvent({
|
|
178
|
+
type: "TASK_COMPLETED",
|
|
179
|
+
at: Date.now(),
|
|
180
|
+
source: "tool",
|
|
181
|
+
entityType: "task",
|
|
182
|
+
entityId: id,
|
|
183
|
+
payload: { id },
|
|
184
|
+
});
|
|
185
|
+
return this.tasks.get(id);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
reopen(id) {
|
|
189
|
+
return this.withLock(() => {
|
|
190
|
+
const entry = this.tasks.get(id);
|
|
191
|
+
if (!entry)
|
|
192
|
+
return undefined;
|
|
193
|
+
this.applyReducerEvent({
|
|
194
|
+
type: "TASK_REOPENED",
|
|
195
|
+
at: Date.now(),
|
|
196
|
+
source: "tool",
|
|
197
|
+
entityType: "task",
|
|
198
|
+
entityId: id,
|
|
199
|
+
payload: { id },
|
|
200
|
+
});
|
|
201
|
+
return this.tasks.get(id);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
updateDetails(id, fields) {
|
|
205
|
+
return this.withLock(() => {
|
|
206
|
+
const entry = this.tasks.get(id);
|
|
207
|
+
if (!entry)
|
|
208
|
+
return undefined;
|
|
209
|
+
if (fields.subject === undefined && fields.description === undefined)
|
|
210
|
+
return entry;
|
|
211
|
+
this.applyReducerEvent({
|
|
212
|
+
type: "TASK_UPDATED",
|
|
213
|
+
at: Date.now(),
|
|
214
|
+
source: "tool",
|
|
215
|
+
entityType: "task",
|
|
216
|
+
entityId: id,
|
|
217
|
+
payload: {
|
|
218
|
+
id,
|
|
219
|
+
subject: fields.subject,
|
|
220
|
+
description: fields.description,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
194
223
|
return this.tasks.get(id);
|
|
195
224
|
});
|
|
196
225
|
}
|
|
@@ -217,7 +246,7 @@ export class TaskStore {
|
|
|
217
246
|
}
|
|
218
247
|
return count;
|
|
219
248
|
}
|
|
220
|
-
|
|
249
|
+
pruneCompleted() {
|
|
221
250
|
return this.withLock(() => {
|
|
222
251
|
const before = this.tasks.size;
|
|
223
252
|
this.applyReducerEvent({
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { LoopEntry, Trigger } from "../types.js";
|
|
3
|
+
interface LoopStoreLike {
|
|
4
|
+
list(): LoopEntry[];
|
|
5
|
+
get(id: string): LoopEntry | undefined;
|
|
6
|
+
create(trigger: Trigger, prompt: string, opts: {
|
|
7
|
+
recurring: boolean;
|
|
8
|
+
autoTask?: boolean;
|
|
9
|
+
taskBacklog?: boolean;
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
maxFires?: number;
|
|
12
|
+
}): LoopEntry;
|
|
13
|
+
pause(id: string): LoopEntry | undefined;
|
|
14
|
+
delete(id: string): boolean;
|
|
15
|
+
}
|
|
16
|
+
interface TriggerSystemLike {
|
|
17
|
+
add(entry: LoopEntry): void;
|
|
18
|
+
remove(id: string): void;
|
|
19
|
+
}
|
|
20
|
+
interface SchedulerLike {
|
|
21
|
+
nextFire(id: string): number | undefined;
|
|
22
|
+
}
|
|
23
|
+
interface MonitorLike {
|
|
24
|
+
id: string;
|
|
25
|
+
status: string;
|
|
26
|
+
}
|
|
27
|
+
interface MonitorManagerLike {
|
|
28
|
+
get(id: string): MonitorLike | undefined;
|
|
29
|
+
}
|
|
30
|
+
export interface LoopToolsOptions {
|
|
31
|
+
pi: ExtensionAPI;
|
|
32
|
+
getStore: () => LoopStoreLike;
|
|
33
|
+
getTriggerSystem: () => TriggerSystemLike;
|
|
34
|
+
getScheduler: () => SchedulerLike;
|
|
35
|
+
getMonitorManager: () => MonitorManagerLike;
|
|
36
|
+
updateWidget: () => void;
|
|
37
|
+
maybeBootstrapTaskLoop: (entry: LoopEntry) => Promise<boolean>;
|
|
38
|
+
isTaskSystemReady: () => boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function registerLoopTools(options: LoopToolsOptions): void;
|
|
41
|
+
export {};
|