@trevonistrevon/pi-loop 0.4.11 → 0.5.0
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 +10 -0
- package/dist/coordinator.d.ts +35 -0
- package/dist/coordinator.js +56 -0
- package/dist/goal-types.d.ts +78 -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 +170 -40
- 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/store.d.ts +2 -0
- package/dist/store.js +118 -44
- 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 +2 -0
- package/dist/task-store.js +82 -30
- 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/coordinator.ts +115 -0
- package/src/goal-types.ts +99 -0
- package/src/goal-verifier.ts +241 -0
- package/src/index.ts +196 -39
- 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/store.ts +119 -43
- package/src/task-backlog-coordinator.ts +32 -0
- package/src/task-reducer.ts +152 -0
- package/src/task-store.ts +84 -27
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) {
|
|
@@ -145,31 +155,75 @@ export class LoopStore {
|
|
|
145
155
|
if (!entry)
|
|
146
156
|
return { entry: undefined, changedFields: [] };
|
|
147
157
|
const changedFields = [];
|
|
148
|
-
|
|
149
|
-
|
|
158
|
+
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
|
+
});
|
|
150
168
|
changedFields.push("status");
|
|
151
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 };
|
|
152
184
|
if (fields.trigger !== undefined) {
|
|
153
|
-
|
|
185
|
+
current.trigger = fields.trigger;
|
|
154
186
|
changedFields.push("trigger");
|
|
155
187
|
}
|
|
156
188
|
if (fields.prompt !== undefined) {
|
|
157
|
-
|
|
189
|
+
current.prompt = fields.prompt;
|
|
158
190
|
changedFields.push("prompt");
|
|
159
191
|
}
|
|
160
192
|
if (fields.fireCount !== undefined) {
|
|
161
|
-
|
|
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
|
+
}
|
|
162
207
|
changedFields.push("fireCount");
|
|
163
208
|
}
|
|
164
|
-
|
|
165
|
-
|
|
209
|
+
if (fields.trigger !== undefined || fields.prompt !== undefined) {
|
|
210
|
+
current.updatedAt = now;
|
|
211
|
+
}
|
|
212
|
+
return { entry: this.loops.get(id), changedFields };
|
|
166
213
|
});
|
|
167
214
|
}
|
|
168
215
|
delete(id) {
|
|
169
216
|
return this.withLock(() => {
|
|
170
217
|
if (!this.loops.has(id))
|
|
171
218
|
return false;
|
|
172
|
-
this.
|
|
219
|
+
this.applyReducerEvent({
|
|
220
|
+
type: "LOOP_DELETED",
|
|
221
|
+
at: Date.now(),
|
|
222
|
+
source: "tool",
|
|
223
|
+
entityType: "loop",
|
|
224
|
+
entityId: id,
|
|
225
|
+
payload: { id },
|
|
226
|
+
});
|
|
173
227
|
return true;
|
|
174
228
|
});
|
|
175
229
|
}
|
|
@@ -177,11 +231,18 @@ export class LoopStore {
|
|
|
177
231
|
return this.withLock(() => {
|
|
178
232
|
const now = Date.now();
|
|
179
233
|
let count = 0;
|
|
180
|
-
for (const [id, entry] of this.loops) {
|
|
181
|
-
if (now
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
234
|
+
for (const [id, entry] of [...this.loops.entries()]) {
|
|
235
|
+
if (now < entry.expiresAt)
|
|
236
|
+
continue;
|
|
237
|
+
this.applyReducerEvent({
|
|
238
|
+
type: "LOOP_EXPIRED",
|
|
239
|
+
at: now,
|
|
240
|
+
source: "system",
|
|
241
|
+
entityType: "loop",
|
|
242
|
+
entityId: id,
|
|
243
|
+
payload: { id, reason: "expires_at" },
|
|
244
|
+
});
|
|
245
|
+
count++;
|
|
185
246
|
}
|
|
186
247
|
return count;
|
|
187
248
|
});
|
|
@@ -189,27 +250,40 @@ export class LoopStore {
|
|
|
189
250
|
expireEventLoops(sessionStartedAt) {
|
|
190
251
|
return this.withLock(() => {
|
|
191
252
|
let count = 0;
|
|
192
|
-
const
|
|
193
|
-
for (const [id, entry] of this.loops) {
|
|
253
|
+
for (const [id, entry] of [...this.loops.entries()]) {
|
|
194
254
|
if (entry.status !== "active")
|
|
195
255
|
continue;
|
|
196
|
-
if (entry.trigger.type
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
256
|
+
if (entry.trigger.type !== "event" && entry.trigger.type !== "hybrid")
|
|
257
|
+
continue;
|
|
258
|
+
if (entry.createdAt >= sessionStartedAt)
|
|
259
|
+
continue;
|
|
260
|
+
this.applyReducerEvent({
|
|
261
|
+
type: "LOOP_EXPIRED",
|
|
262
|
+
at: sessionStartedAt,
|
|
263
|
+
source: "session",
|
|
264
|
+
entityType: "loop",
|
|
265
|
+
entityId: id,
|
|
266
|
+
payload: { id, reason: "resume_event_stale" },
|
|
267
|
+
});
|
|
268
|
+
count++;
|
|
202
269
|
}
|
|
203
|
-
for (const id of toDelete)
|
|
204
|
-
this.loops.delete(id);
|
|
205
270
|
return count;
|
|
206
271
|
});
|
|
207
272
|
}
|
|
208
273
|
clearAll() {
|
|
209
274
|
return this.withLock(() => {
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
275
|
+
const ids = [...this.loops.keys()];
|
|
276
|
+
for (const id of ids) {
|
|
277
|
+
this.applyReducerEvent({
|
|
278
|
+
type: "LOOP_DELETED",
|
|
279
|
+
at: Date.now(),
|
|
280
|
+
source: "system",
|
|
281
|
+
entityType: "loop",
|
|
282
|
+
entityId: id,
|
|
283
|
+
payload: { id },
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return ids.length;
|
|
213
287
|
});
|
|
214
288
|
}
|
|
215
289
|
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
|
@@ -8,6 +8,8 @@ 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[];
|
package/dist/task-store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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 { reduceTaskState } from "./task-reducer.js";
|
|
4
5
|
const TASKS_DIR = join(homedir(), ".pi", "tasks");
|
|
5
6
|
const LOCK_RETRY_MS = 50;
|
|
6
7
|
const LOCK_MAX_RETRIES = 100;
|
|
@@ -103,23 +104,31 @@ export class TaskStore {
|
|
|
103
104
|
releaseLock(this.lockPath);
|
|
104
105
|
}
|
|
105
106
|
}
|
|
107
|
+
toReducerState() {
|
|
108
|
+
return {
|
|
109
|
+
nextId: this.nextId,
|
|
110
|
+
tasksById: Object.fromEntries(this.tasks.entries()),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
applyReducerEvent(event) {
|
|
114
|
+
const result = reduceTaskState(this.toReducerState(), event);
|
|
115
|
+
this.nextId = result.state.nextId;
|
|
116
|
+
this.tasks = new Map(Object.entries(result.state.tasksById));
|
|
117
|
+
}
|
|
106
118
|
create(subject, description, metadata) {
|
|
107
119
|
return this.withLock(() => {
|
|
108
120
|
if (this.tasks.size >= MAX_TASKS) {
|
|
109
121
|
throw new Error(`Maximum of ${MAX_TASKS} tasks reached. Delete some before creating new ones.`);
|
|
110
122
|
}
|
|
111
123
|
const now = Date.now();
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
};
|
|
121
|
-
this.tasks.set(entry.id, entry);
|
|
122
|
-
return entry;
|
|
124
|
+
this.applyReducerEvent({
|
|
125
|
+
type: "TASK_CREATED",
|
|
126
|
+
at: now,
|
|
127
|
+
source: "tool",
|
|
128
|
+
entityType: "task",
|
|
129
|
+
payload: { subject, description, metadata },
|
|
130
|
+
});
|
|
131
|
+
return this.tasks.get(String(this.nextId - 1));
|
|
123
132
|
});
|
|
124
133
|
}
|
|
125
134
|
get(id) {
|
|
@@ -137,24 +146,66 @@ export class TaskStore {
|
|
|
137
146
|
const entry = this.tasks.get(id);
|
|
138
147
|
if (!entry)
|
|
139
148
|
return undefined;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
if (fields.status === "in_progress") {
|
|
151
|
+
this.applyReducerEvent({
|
|
152
|
+
type: "TASK_STARTED",
|
|
153
|
+
at: now,
|
|
154
|
+
source: "tool",
|
|
155
|
+
entityType: "task",
|
|
156
|
+
entityId: id,
|
|
157
|
+
payload: { id },
|
|
158
|
+
});
|
|
144
159
|
}
|
|
145
|
-
if (fields.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
else if (fields.status === "completed") {
|
|
161
|
+
this.applyReducerEvent({
|
|
162
|
+
type: "TASK_COMPLETED",
|
|
163
|
+
at: now,
|
|
164
|
+
source: "tool",
|
|
165
|
+
entityType: "task",
|
|
166
|
+
entityId: id,
|
|
167
|
+
payload: { id },
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
else if (fields.status === "pending") {
|
|
171
|
+
this.applyReducerEvent({
|
|
172
|
+
type: "TASK_REOPENED",
|
|
173
|
+
at: now,
|
|
174
|
+
source: "tool",
|
|
175
|
+
entityType: "task",
|
|
176
|
+
entityId: id,
|
|
177
|
+
payload: { id },
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (fields.subject !== undefined || fields.description !== undefined) {
|
|
181
|
+
this.applyReducerEvent({
|
|
182
|
+
type: "TASK_UPDATED",
|
|
183
|
+
at: now,
|
|
184
|
+
source: "tool",
|
|
185
|
+
entityType: "task",
|
|
186
|
+
entityId: id,
|
|
187
|
+
payload: {
|
|
188
|
+
id,
|
|
189
|
+
subject: fields.subject,
|
|
190
|
+
description: fields.description,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return this.tasks.get(id);
|
|
151
195
|
});
|
|
152
196
|
}
|
|
153
197
|
delete(id) {
|
|
154
198
|
return this.withLock(() => {
|
|
155
199
|
if (!this.tasks.has(id))
|
|
156
200
|
return false;
|
|
157
|
-
this.
|
|
201
|
+
this.applyReducerEvent({
|
|
202
|
+
type: "TASK_DELETED",
|
|
203
|
+
at: Date.now(),
|
|
204
|
+
source: "tool",
|
|
205
|
+
entityType: "task",
|
|
206
|
+
entityId: id,
|
|
207
|
+
payload: { id },
|
|
208
|
+
});
|
|
158
209
|
return true;
|
|
159
210
|
});
|
|
160
211
|
}
|
|
@@ -168,14 +219,15 @@ export class TaskStore {
|
|
|
168
219
|
}
|
|
169
220
|
sweepCompleted() {
|
|
170
221
|
return this.withLock(() => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
222
|
+
const before = this.tasks.size;
|
|
223
|
+
this.applyReducerEvent({
|
|
224
|
+
type: "TASKS_PRUNED",
|
|
225
|
+
at: Date.now(),
|
|
226
|
+
source: "system",
|
|
227
|
+
entityType: "task",
|
|
228
|
+
payload: { reason: "manual" },
|
|
229
|
+
});
|
|
230
|
+
return before - this.tasks.size;
|
|
179
231
|
});
|
|
180
232
|
}
|
|
181
233
|
}
|