@trevonistrevon/pi-loop 0.4.11 → 0.5.1
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 +20 -9
- 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/coordinator.d.ts +35 -0
- package/dist/coordinator.js +56 -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 +31 -0
- package/dist/goal-store.js +298 -0
- package/dist/goal-types.d.ts +82 -0
- package/dist/goal-types.js +1 -0
- package/dist/goal-verifier.d.ts +20 -0
- package/dist/goal-verifier.js +198 -0
- package/dist/index.js +130 -1087
- package/dist/loop-reducer.d.ts +63 -0
- package/dist/loop-reducer.js +67 -0
- package/dist/monitor-completion-coordinator.d.ts +10 -0
- package/dist/monitor-completion-coordinator.js +13 -0
- package/dist/monitor-manager.d.ts +2 -0
- package/dist/monitor-manager.js +107 -29
- package/dist/monitor-reducer.d.ts +82 -0
- package/dist/monitor-reducer.js +69 -0
- package/dist/notification-reducer.d.ts +81 -0
- package/dist/notification-reducer.js +65 -0
- 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 +129 -49
- package/dist/task-backlog-coordinator.d.ts +12 -0
- package/dist/task-backlog-coordinator.js +22 -0
- package/dist/task-reducer.d.ts +66 -0
- package/dist/task-reducer.js +76 -0
- package/dist/task-store.d.ts +8 -4
- package/dist/task-store.js +102 -33
- 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 +127 -0
- package/docs/architecture/goal-state-schema.md +505 -0
- package/docs/architecture/state-machine-migration.md +546 -0
- package/docs/architecture/state-machine-reducer-event-model.md +823 -0
- package/docs/architecture/state-machine-test-matrix.md +249 -0
- package/docs/architecture/state-machine-transition-map.md +436 -0
- package/package.json +1 -1
- package/src/commands/loop-command.ts +184 -0
- package/src/commands/tasks-command.ts +135 -0
- package/src/coordinator.ts +115 -0
- package/src/goal-coordinator.ts +58 -0
- package/src/goal-reducer.ts +280 -0
- package/src/goal-store.ts +315 -0
- package/src/goal-types.ts +104 -0
- package/src/goal-verifier.ts +241 -0
- package/src/index.ts +134 -1147
- package/src/loop-reducer.ts +148 -0
- package/src/monitor-completion-coordinator.ts +24 -0
- package/src/monitor-manager.ts +115 -27
- package/src/monitor-reducer.ts +166 -0
- package/src/notification-reducer.ts +155 -0
- 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 +132 -50
- package/src/task-backlog-coordinator.ts +32 -0
- package/src/task-reducer.ts +152 -0
- package/src/task-store.ts +103 -31
- package/src/tools/loop-tools.ts +304 -0
- package/src/tools/monitor-tools.ts +145 -0
- package/src/tools/native-task-tools.ts +144 -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,4 +1,4 @@
|
|
|
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;
|
|
@@ -8,6 +8,8 @@ export declare class LoopStore {
|
|
|
8
8
|
private load;
|
|
9
9
|
private save;
|
|
10
10
|
private withLock;
|
|
11
|
+
private toReducerState;
|
|
12
|
+
private applyReducerEvent;
|
|
11
13
|
create(trigger: Trigger, prompt: string, opts: {
|
|
12
14
|
recurring: boolean;
|
|
13
15
|
autoTask?: boolean;
|
|
@@ -17,11 +19,12 @@ export declare class LoopStore {
|
|
|
17
19
|
}): LoopEntry;
|
|
18
20
|
get(id: string): LoopEntry | undefined;
|
|
19
21
|
list(): LoopEntry[];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
pause(id: string): LoopEntry | undefined;
|
|
23
|
+
resume(id: string): LoopEntry | undefined;
|
|
24
|
+
fire(id: string): LoopEntry | undefined;
|
|
25
|
+
updateMetadata(id: string, fields: {
|
|
22
26
|
trigger?: Trigger;
|
|
23
27
|
prompt?: string;
|
|
24
|
-
fireCount?: number;
|
|
25
28
|
}): {
|
|
26
29
|
entry: LoopEntry | undefined;
|
|
27
30
|
changedFields: string[];
|
package/dist/store.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
4
|
+
import { reduceLoopState } from "./loop-reducer.js";
|
|
4
5
|
const LOOPS_DIR = join(homedir(), ".pi", "loops");
|
|
5
6
|
const LOCK_RETRY_MS = 50;
|
|
6
7
|
const LOCK_MAX_RETRIES = 100;
|
|
7
8
|
const MAX_LOOPS = 25;
|
|
8
|
-
const MAX_EXPIRY_DAYS = 7;
|
|
9
9
|
function acquireLock(lockPath) {
|
|
10
10
|
for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
|
|
11
11
|
try {
|
|
@@ -104,29 +104,39 @@ export class LoopStore {
|
|
|
104
104
|
releaseLock(this.lockPath);
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
toReducerState() {
|
|
108
|
+
return {
|
|
109
|
+
nextId: this.nextId,
|
|
110
|
+
loopsById: Object.fromEntries(this.loops.entries()),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
applyReducerEvent(event) {
|
|
114
|
+
const result = reduceLoopState(this.toReducerState(), event);
|
|
115
|
+
this.nextId = result.state.nextId;
|
|
116
|
+
this.loops = new Map(Object.entries(result.state.loopsById));
|
|
117
|
+
}
|
|
107
118
|
create(trigger, prompt, opts) {
|
|
108
119
|
return this.withLock(() => {
|
|
109
120
|
if (this.loops.size >= MAX_LOOPS) {
|
|
110
121
|
throw new Error(`Maximum of ${MAX_LOOPS} loops reached. Delete some before creating new ones.`);
|
|
111
122
|
}
|
|
112
123
|
const now = Date.now();
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
};
|
|
128
|
-
this.loops.
|
|
129
|
-
return entry;
|
|
124
|
+
this.applyReducerEvent({
|
|
125
|
+
type: "LOOP_CREATED",
|
|
126
|
+
at: now,
|
|
127
|
+
source: "tool",
|
|
128
|
+
entityType: "loop",
|
|
129
|
+
payload: {
|
|
130
|
+
prompt,
|
|
131
|
+
trigger,
|
|
132
|
+
recurring: opts.recurring,
|
|
133
|
+
autoTask: opts.autoTask,
|
|
134
|
+
taskBacklog: opts.taskBacklog,
|
|
135
|
+
readOnly: opts.readOnly,
|
|
136
|
+
maxFires: opts.maxFires,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
return this.loops.get(String(this.nextId - 1));
|
|
130
140
|
});
|
|
131
141
|
}
|
|
132
142
|
get(id) {
|
|
@@ -139,37 +149,87 @@ export class LoopStore {
|
|
|
139
149
|
this.load();
|
|
140
150
|
return Array.from(this.loops.values()).sort((a, b) => Number(a.id) - Number(b.id));
|
|
141
151
|
}
|
|
142
|
-
|
|
152
|
+
pause(id) {
|
|
153
|
+
return this.withLock(() => {
|
|
154
|
+
const entry = this.loops.get(id);
|
|
155
|
+
if (!entry)
|
|
156
|
+
return undefined;
|
|
157
|
+
this.applyReducerEvent({
|
|
158
|
+
type: "LOOP_PAUSED",
|
|
159
|
+
at: Date.now(),
|
|
160
|
+
source: "tool",
|
|
161
|
+
entityType: "loop",
|
|
162
|
+
entityId: id,
|
|
163
|
+
payload: { id },
|
|
164
|
+
});
|
|
165
|
+
return this.loops.get(id);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
resume(id) {
|
|
169
|
+
return this.withLock(() => {
|
|
170
|
+
const entry = this.loops.get(id);
|
|
171
|
+
if (!entry)
|
|
172
|
+
return undefined;
|
|
173
|
+
this.applyReducerEvent({
|
|
174
|
+
type: "LOOP_RESUMED",
|
|
175
|
+
at: Date.now(),
|
|
176
|
+
source: "tool",
|
|
177
|
+
entityType: "loop",
|
|
178
|
+
entityId: id,
|
|
179
|
+
payload: { id },
|
|
180
|
+
});
|
|
181
|
+
return this.loops.get(id);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
fire(id) {
|
|
143
185
|
return this.withLock(() => {
|
|
144
186
|
const entry = this.loops.get(id);
|
|
145
187
|
if (!entry)
|
|
188
|
+
return undefined;
|
|
189
|
+
this.applyReducerEvent({
|
|
190
|
+
type: "LOOP_FIRED",
|
|
191
|
+
at: Date.now(),
|
|
192
|
+
source: "system",
|
|
193
|
+
entityType: "loop",
|
|
194
|
+
entityId: id,
|
|
195
|
+
payload: { id },
|
|
196
|
+
});
|
|
197
|
+
return this.loops.get(id);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
updateMetadata(id, fields) {
|
|
201
|
+
return this.withLock(() => {
|
|
202
|
+
const current = this.loops.get(id);
|
|
203
|
+
if (!current)
|
|
146
204
|
return { entry: undefined, changedFields: [] };
|
|
147
205
|
const changedFields = [];
|
|
148
|
-
|
|
149
|
-
entry.status = fields.status;
|
|
150
|
-
changedFields.push("status");
|
|
151
|
-
}
|
|
206
|
+
const now = Date.now();
|
|
152
207
|
if (fields.trigger !== undefined) {
|
|
153
|
-
|
|
208
|
+
current.trigger = fields.trigger;
|
|
154
209
|
changedFields.push("trigger");
|
|
155
210
|
}
|
|
156
211
|
if (fields.prompt !== undefined) {
|
|
157
|
-
|
|
212
|
+
current.prompt = fields.prompt;
|
|
158
213
|
changedFields.push("prompt");
|
|
159
214
|
}
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
changedFields.push("fireCount");
|
|
215
|
+
if (changedFields.length > 0) {
|
|
216
|
+
current.updatedAt = now;
|
|
163
217
|
}
|
|
164
|
-
entry
|
|
165
|
-
return { entry, changedFields };
|
|
218
|
+
return { entry: this.loops.get(id), changedFields };
|
|
166
219
|
});
|
|
167
220
|
}
|
|
168
221
|
delete(id) {
|
|
169
222
|
return this.withLock(() => {
|
|
170
223
|
if (!this.loops.has(id))
|
|
171
224
|
return false;
|
|
172
|
-
this.
|
|
225
|
+
this.applyReducerEvent({
|
|
226
|
+
type: "LOOP_DELETED",
|
|
227
|
+
at: Date.now(),
|
|
228
|
+
source: "tool",
|
|
229
|
+
entityType: "loop",
|
|
230
|
+
entityId: id,
|
|
231
|
+
payload: { id },
|
|
232
|
+
});
|
|
173
233
|
return true;
|
|
174
234
|
});
|
|
175
235
|
}
|
|
@@ -177,11 +237,18 @@ export class LoopStore {
|
|
|
177
237
|
return this.withLock(() => {
|
|
178
238
|
const now = Date.now();
|
|
179
239
|
let count = 0;
|
|
180
|
-
for (const [id, entry] of this.loops) {
|
|
181
|
-
if (now
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
240
|
+
for (const [id, entry] of [...this.loops.entries()]) {
|
|
241
|
+
if (now < entry.expiresAt)
|
|
242
|
+
continue;
|
|
243
|
+
this.applyReducerEvent({
|
|
244
|
+
type: "LOOP_EXPIRED",
|
|
245
|
+
at: now,
|
|
246
|
+
source: "system",
|
|
247
|
+
entityType: "loop",
|
|
248
|
+
entityId: id,
|
|
249
|
+
payload: { id, reason: "expires_at" },
|
|
250
|
+
});
|
|
251
|
+
count++;
|
|
185
252
|
}
|
|
186
253
|
return count;
|
|
187
254
|
});
|
|
@@ -189,27 +256,40 @@ export class LoopStore {
|
|
|
189
256
|
expireEventLoops(sessionStartedAt) {
|
|
190
257
|
return this.withLock(() => {
|
|
191
258
|
let count = 0;
|
|
192
|
-
const
|
|
193
|
-
for (const [id, entry] of this.loops) {
|
|
259
|
+
for (const [id, entry] of [...this.loops.entries()]) {
|
|
194
260
|
if (entry.status !== "active")
|
|
195
261
|
continue;
|
|
196
|
-
if (entry.trigger.type
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
262
|
+
if (entry.trigger.type !== "event" && entry.trigger.type !== "hybrid")
|
|
263
|
+
continue;
|
|
264
|
+
if (entry.createdAt >= sessionStartedAt)
|
|
265
|
+
continue;
|
|
266
|
+
this.applyReducerEvent({
|
|
267
|
+
type: "LOOP_EXPIRED",
|
|
268
|
+
at: sessionStartedAt,
|
|
269
|
+
source: "session",
|
|
270
|
+
entityType: "loop",
|
|
271
|
+
entityId: id,
|
|
272
|
+
payload: { id, reason: "resume_event_stale" },
|
|
273
|
+
});
|
|
274
|
+
count++;
|
|
202
275
|
}
|
|
203
|
-
for (const id of toDelete)
|
|
204
|
-
this.loops.delete(id);
|
|
205
276
|
return count;
|
|
206
277
|
});
|
|
207
278
|
}
|
|
208
279
|
clearAll() {
|
|
209
280
|
return this.withLock(() => {
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
281
|
+
const ids = [...this.loops.keys()];
|
|
282
|
+
for (const id of ids) {
|
|
283
|
+
this.applyReducerEvent({
|
|
284
|
+
type: "LOOP_DELETED",
|
|
285
|
+
at: Date.now(),
|
|
286
|
+
source: "system",
|
|
287
|
+
entityType: "loop",
|
|
288
|
+
entityId: id,
|
|
289
|
+
payload: { id },
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return ids.length;
|
|
213
293
|
});
|
|
214
294
|
}
|
|
215
295
|
deleteFileIfEmpty() {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReducerEffect, ReducerEvent } from "./coordinator.js";
|
|
2
|
+
export type TaskBacklogEvent = ReducerEvent<"TASK_BACKLOG_EVALUATED", {
|
|
3
|
+
pendingCount: number;
|
|
4
|
+
threshold: number;
|
|
5
|
+
}>;
|
|
6
|
+
export type TaskBacklogEffect = ReducerEffect<"ENSURE_AUTO_TASK_WORKER", {
|
|
7
|
+
pendingCount: number;
|
|
8
|
+
threshold: number;
|
|
9
|
+
}> | ReducerEffect<"CLEANUP_TASK_BACKLOG_LOOPS", {
|
|
10
|
+
pendingCount: number;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function reduceTaskBacklogEvent(event: TaskBacklogEvent): TaskBacklogEffect[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function reduceTaskBacklogEvent(event) {
|
|
2
|
+
if (event.type !== "TASK_BACKLOG_EVALUATED")
|
|
3
|
+
return [];
|
|
4
|
+
const { pendingCount, threshold } = event.payload;
|
|
5
|
+
if (pendingCount < 0)
|
|
6
|
+
return [];
|
|
7
|
+
if (pendingCount === 0) {
|
|
8
|
+
return [{
|
|
9
|
+
type: "CLEANUP_TASK_BACKLOG_LOOPS",
|
|
10
|
+
entityType: "task",
|
|
11
|
+
payload: { pendingCount },
|
|
12
|
+
}];
|
|
13
|
+
}
|
|
14
|
+
if (pendingCount >= threshold) {
|
|
15
|
+
return [{
|
|
16
|
+
type: "ENSURE_AUTO_TASK_WORKER",
|
|
17
|
+
entityType: "task",
|
|
18
|
+
payload: { pendingCount, threshold },
|
|
19
|
+
}];
|
|
20
|
+
}
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { TaskEntry } from "./task-types.js";
|
|
2
|
+
export interface TaskReducerState {
|
|
3
|
+
nextId: number;
|
|
4
|
+
tasksById: Record<string, TaskEntry>;
|
|
5
|
+
}
|
|
6
|
+
export type TaskReducerEvent = {
|
|
7
|
+
type: "TASK_CREATED";
|
|
8
|
+
at: number;
|
|
9
|
+
source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
10
|
+
entityType?: "task";
|
|
11
|
+
entityId?: string;
|
|
12
|
+
payload: {
|
|
13
|
+
subject: string;
|
|
14
|
+
description: string;
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
} | {
|
|
18
|
+
type: "TASK_STARTED" | "TASK_COMPLETED" | "TASK_REOPENED" | "TASK_DELETED";
|
|
19
|
+
at: number;
|
|
20
|
+
source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
21
|
+
entityType?: "task";
|
|
22
|
+
entityId?: string;
|
|
23
|
+
payload: {
|
|
24
|
+
id: string;
|
|
25
|
+
};
|
|
26
|
+
} | {
|
|
27
|
+
type: "TASK_UPDATED";
|
|
28
|
+
at: number;
|
|
29
|
+
source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
30
|
+
entityType?: "task";
|
|
31
|
+
entityId?: string;
|
|
32
|
+
payload: {
|
|
33
|
+
id: string;
|
|
34
|
+
subject?: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
};
|
|
37
|
+
} | {
|
|
38
|
+
type: "TASKS_PRUNED";
|
|
39
|
+
at: number;
|
|
40
|
+
source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
41
|
+
entityType?: "task";
|
|
42
|
+
entityId?: string;
|
|
43
|
+
payload: {
|
|
44
|
+
reason: "git_commit" | "zero_pending_cleanup" | "manual";
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
export type TaskReducerEffect = {
|
|
48
|
+
type: "PERSIST_TASK";
|
|
49
|
+
entityType: "task";
|
|
50
|
+
entityId: string;
|
|
51
|
+
payload: {
|
|
52
|
+
task: TaskEntry;
|
|
53
|
+
};
|
|
54
|
+
} | {
|
|
55
|
+
type: "DELETE_TASK";
|
|
56
|
+
entityType: "task";
|
|
57
|
+
entityId: string;
|
|
58
|
+
payload: {
|
|
59
|
+
id: string;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
export interface TaskReduceResult {
|
|
63
|
+
state: TaskReducerState;
|
|
64
|
+
effects: TaskReducerEffect[];
|
|
65
|
+
}
|
|
66
|
+
export declare function reduceTaskState(state: TaskReducerState, event: TaskReducerEvent): TaskReduceResult;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
function cloneState(state) {
|
|
2
|
+
return {
|
|
3
|
+
nextId: state.nextId,
|
|
4
|
+
tasksById: { ...state.tasksById },
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export function reduceTaskState(state, event) {
|
|
8
|
+
if (event.type === "TASK_CREATED") {
|
|
9
|
+
const next = cloneState(state);
|
|
10
|
+
const id = String(next.nextId++);
|
|
11
|
+
const task = {
|
|
12
|
+
id,
|
|
13
|
+
subject: event.payload.subject,
|
|
14
|
+
description: event.payload.description,
|
|
15
|
+
status: "pending",
|
|
16
|
+
createdAt: event.at,
|
|
17
|
+
updatedAt: event.at,
|
|
18
|
+
metadata: event.payload.metadata,
|
|
19
|
+
};
|
|
20
|
+
next.tasksById[id] = task;
|
|
21
|
+
return {
|
|
22
|
+
state: next,
|
|
23
|
+
effects: [{ type: "PERSIST_TASK", entityType: "task", entityId: id, payload: { task } }],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (event.type === "TASKS_PRUNED") {
|
|
27
|
+
const next = cloneState(state);
|
|
28
|
+
const effects = [];
|
|
29
|
+
for (const [id, task] of Object.entries(next.tasksById)) {
|
|
30
|
+
if (task.status !== "completed")
|
|
31
|
+
continue;
|
|
32
|
+
delete next.tasksById[id];
|
|
33
|
+
effects.push({ type: "DELETE_TASK", entityType: "task", entityId: id, payload: { id } });
|
|
34
|
+
}
|
|
35
|
+
return { state: next, effects };
|
|
36
|
+
}
|
|
37
|
+
const id = event.payload.id;
|
|
38
|
+
const current = state.tasksById[id];
|
|
39
|
+
if (!current)
|
|
40
|
+
return { state, effects: [] };
|
|
41
|
+
if (event.type === "TASK_DELETED") {
|
|
42
|
+
const next = cloneState(state);
|
|
43
|
+
delete next.tasksById[id];
|
|
44
|
+
return {
|
|
45
|
+
state: next,
|
|
46
|
+
effects: [{ type: "DELETE_TASK", entityType: "task", entityId: id, payload: { id } }],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const next = cloneState(state);
|
|
50
|
+
const task = { ...current };
|
|
51
|
+
if (event.type === "TASK_STARTED") {
|
|
52
|
+
task.status = "in_progress";
|
|
53
|
+
task.updatedAt = event.at;
|
|
54
|
+
}
|
|
55
|
+
if (event.type === "TASK_COMPLETED") {
|
|
56
|
+
task.status = "completed";
|
|
57
|
+
task.updatedAt = event.at;
|
|
58
|
+
task.completedAt = event.at;
|
|
59
|
+
}
|
|
60
|
+
if (event.type === "TASK_REOPENED") {
|
|
61
|
+
task.status = "pending";
|
|
62
|
+
task.updatedAt = event.at;
|
|
63
|
+
}
|
|
64
|
+
if (event.type === "TASK_UPDATED") {
|
|
65
|
+
if (event.payload.subject !== undefined)
|
|
66
|
+
task.subject = event.payload.subject;
|
|
67
|
+
if (event.payload.description !== undefined)
|
|
68
|
+
task.description = event.payload.description;
|
|
69
|
+
task.updatedAt = event.at;
|
|
70
|
+
}
|
|
71
|
+
next.tasksById[id] = task;
|
|
72
|
+
return {
|
|
73
|
+
state: next,
|
|
74
|
+
effects: [{ type: "PERSIST_TASK", entityType: "task", entityId: id, payload: { task } }],
|
|
75
|
+
};
|
|
76
|
+
}
|
package/dist/task-store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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;
|
|
@@ -8,15 +8,19 @@ export declare class TaskStore {
|
|
|
8
8
|
private load;
|
|
9
9
|
private save;
|
|
10
10
|
private withLock;
|
|
11
|
+
private toReducerState;
|
|
12
|
+
private applyReducerEvent;
|
|
11
13
|
create(subject: string, description: string, metadata?: Record<string, unknown>): TaskEntry;
|
|
12
14
|
get(id: string): TaskEntry | undefined;
|
|
13
15
|
list(): TaskEntry[];
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
start(id: string): TaskEntry | undefined;
|
|
17
|
+
complete(id: string): TaskEntry | undefined;
|
|
18
|
+
reopen(id: string): TaskEntry | undefined;
|
|
19
|
+
updateDetails(id: string, fields: {
|
|
16
20
|
subject?: string;
|
|
17
21
|
description?: string;
|
|
18
22
|
}): TaskEntry | undefined;
|
|
19
23
|
delete(id: string): boolean;
|
|
20
24
|
pendingCount(): number;
|
|
21
|
-
|
|
25
|
+
pruneCompleted(): number;
|
|
22
26
|
}
|