@trevonistrevon/pi-loop 0.4.10 → 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 +11 -1
- 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 +182 -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 +209 -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/src/store.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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 { type LoopReducerEvent, type LoopReducerState, reduceLoopState } from "./loop-reducer.js";
|
|
4
5
|
import type { LoopEntry, LoopStatus, LoopStoreData, Trigger } from "./types.js";
|
|
5
6
|
|
|
6
7
|
const LOOPS_DIR = join(homedir(), ".pi", "loops");
|
|
7
8
|
const LOCK_RETRY_MS = 50;
|
|
8
9
|
const LOCK_MAX_RETRIES = 100;
|
|
9
10
|
const MAX_LOOPS = 25;
|
|
10
|
-
const MAX_EXPIRY_DAYS = 7;
|
|
11
11
|
|
|
12
12
|
function acquireLock(lockPath: string): void {
|
|
13
13
|
for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
|
|
@@ -95,29 +95,41 @@ export class LoopStore {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
private toReducerState(): LoopReducerState {
|
|
99
|
+
return {
|
|
100
|
+
nextId: this.nextId,
|
|
101
|
+
loopsById: Object.fromEntries(this.loops.entries()),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private applyReducerEvent(event: LoopReducerEvent): void {
|
|
106
|
+
const result = reduceLoopState(this.toReducerState(), event);
|
|
107
|
+
this.nextId = result.state.nextId;
|
|
108
|
+
this.loops = new Map(Object.entries(result.state.loopsById));
|
|
109
|
+
}
|
|
110
|
+
|
|
98
111
|
create(trigger: Trigger, prompt: string, opts: { recurring: boolean; autoTask?: boolean; taskBacklog?: boolean; readOnly?: boolean; maxFires?: number }): LoopEntry {
|
|
99
112
|
return this.withLock(() => {
|
|
100
113
|
if (this.loops.size >= MAX_LOOPS) {
|
|
101
114
|
throw new Error(`Maximum of ${MAX_LOOPS} loops reached. Delete some before creating new ones.`);
|
|
102
115
|
}
|
|
103
116
|
const now = Date.now();
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
};
|
|
119
|
-
this.loops.
|
|
120
|
-
return entry;
|
|
117
|
+
this.applyReducerEvent({
|
|
118
|
+
type: "LOOP_CREATED",
|
|
119
|
+
at: now,
|
|
120
|
+
source: "tool",
|
|
121
|
+
entityType: "loop",
|
|
122
|
+
payload: {
|
|
123
|
+
prompt,
|
|
124
|
+
trigger,
|
|
125
|
+
recurring: opts.recurring,
|
|
126
|
+
autoTask: opts.autoTask,
|
|
127
|
+
taskBacklog: opts.taskBacklog,
|
|
128
|
+
readOnly: opts.readOnly,
|
|
129
|
+
maxFires: opts.maxFires,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
return this.loops.get(String(this.nextId - 1))!;
|
|
121
133
|
});
|
|
122
134
|
}
|
|
123
135
|
|
|
@@ -137,31 +149,77 @@ export class LoopStore {
|
|
|
137
149
|
if (!entry) return { entry: undefined, changedFields: [] };
|
|
138
150
|
|
|
139
151
|
const changedFields: string[] = [];
|
|
140
|
-
|
|
141
|
-
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
|
|
154
|
+
if (fields.status === "paused") {
|
|
155
|
+
this.applyReducerEvent({
|
|
156
|
+
type: "LOOP_PAUSED",
|
|
157
|
+
at: now,
|
|
158
|
+
source: "tool",
|
|
159
|
+
entityType: "loop",
|
|
160
|
+
entityId: id,
|
|
161
|
+
payload: { id },
|
|
162
|
+
});
|
|
163
|
+
changedFields.push("status");
|
|
164
|
+
} else if (fields.status === "active") {
|
|
165
|
+
this.applyReducerEvent({
|
|
166
|
+
type: "LOOP_RESUMED",
|
|
167
|
+
at: now,
|
|
168
|
+
source: "tool",
|
|
169
|
+
entityType: "loop",
|
|
170
|
+
entityId: id,
|
|
171
|
+
payload: { id },
|
|
172
|
+
});
|
|
142
173
|
changedFields.push("status");
|
|
143
174
|
}
|
|
175
|
+
|
|
176
|
+
const current = this.loops.get(id);
|
|
177
|
+
if (!current) return { entry: undefined, changedFields };
|
|
178
|
+
|
|
144
179
|
if (fields.trigger !== undefined) {
|
|
145
|
-
|
|
180
|
+
current.trigger = fields.trigger;
|
|
146
181
|
changedFields.push("trigger");
|
|
147
182
|
}
|
|
148
183
|
if (fields.prompt !== undefined) {
|
|
149
|
-
|
|
184
|
+
current.prompt = fields.prompt;
|
|
150
185
|
changedFields.push("prompt");
|
|
151
186
|
}
|
|
152
187
|
if (fields.fireCount !== undefined) {
|
|
153
|
-
|
|
188
|
+
if (fields.fireCount === (current.fireCount ?? 0) + 1) {
|
|
189
|
+
this.applyReducerEvent({
|
|
190
|
+
type: "LOOP_FIRED",
|
|
191
|
+
at: now,
|
|
192
|
+
source: "system",
|
|
193
|
+
entityType: "loop",
|
|
194
|
+
entityId: id,
|
|
195
|
+
payload: { id },
|
|
196
|
+
});
|
|
197
|
+
} else {
|
|
198
|
+
current.fireCount = fields.fireCount;
|
|
199
|
+
current.updatedAt = now;
|
|
200
|
+
}
|
|
154
201
|
changedFields.push("fireCount");
|
|
155
202
|
}
|
|
156
|
-
|
|
157
|
-
|
|
203
|
+
|
|
204
|
+
if (fields.trigger !== undefined || fields.prompt !== undefined) {
|
|
205
|
+
current.updatedAt = now;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { entry: this.loops.get(id), changedFields };
|
|
158
209
|
});
|
|
159
210
|
}
|
|
160
211
|
|
|
161
212
|
delete(id: string): boolean {
|
|
162
213
|
return this.withLock(() => {
|
|
163
214
|
if (!this.loops.has(id)) return false;
|
|
164
|
-
this.
|
|
215
|
+
this.applyReducerEvent({
|
|
216
|
+
type: "LOOP_DELETED",
|
|
217
|
+
at: Date.now(),
|
|
218
|
+
source: "tool",
|
|
219
|
+
entityType: "loop",
|
|
220
|
+
entityId: id,
|
|
221
|
+
payload: { id },
|
|
222
|
+
});
|
|
165
223
|
return true;
|
|
166
224
|
});
|
|
167
225
|
}
|
|
@@ -170,11 +228,17 @@ export class LoopStore {
|
|
|
170
228
|
return this.withLock(() => {
|
|
171
229
|
const now = Date.now();
|
|
172
230
|
let count = 0;
|
|
173
|
-
for (const [id, entry] of this.loops) {
|
|
174
|
-
if (now
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
231
|
+
for (const [id, entry] of [...this.loops.entries()]) {
|
|
232
|
+
if (now < entry.expiresAt) continue;
|
|
233
|
+
this.applyReducerEvent({
|
|
234
|
+
type: "LOOP_EXPIRED",
|
|
235
|
+
at: now,
|
|
236
|
+
source: "system",
|
|
237
|
+
entityType: "loop",
|
|
238
|
+
entityId: id,
|
|
239
|
+
payload: { id, reason: "expires_at" },
|
|
240
|
+
});
|
|
241
|
+
count++;
|
|
178
242
|
}
|
|
179
243
|
return count;
|
|
180
244
|
});
|
|
@@ -183,26 +247,38 @@ export class LoopStore {
|
|
|
183
247
|
expireEventLoops(sessionStartedAt: number): number {
|
|
184
248
|
return this.withLock(() => {
|
|
185
249
|
let count = 0;
|
|
186
|
-
const
|
|
187
|
-
for (const [id, entry] of this.loops) {
|
|
250
|
+
for (const [id, entry] of [...this.loops.entries()]) {
|
|
188
251
|
if (entry.status !== "active") continue;
|
|
189
|
-
if (entry.trigger.type
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
252
|
+
if (entry.trigger.type !== "event" && entry.trigger.type !== "hybrid") continue;
|
|
253
|
+
if (entry.createdAt >= sessionStartedAt) continue;
|
|
254
|
+
this.applyReducerEvent({
|
|
255
|
+
type: "LOOP_EXPIRED",
|
|
256
|
+
at: sessionStartedAt,
|
|
257
|
+
source: "session",
|
|
258
|
+
entityType: "loop",
|
|
259
|
+
entityId: id,
|
|
260
|
+
payload: { id, reason: "resume_event_stale" },
|
|
261
|
+
});
|
|
262
|
+
count++;
|
|
195
263
|
}
|
|
196
|
-
for (const id of toDelete) this.loops.delete(id);
|
|
197
264
|
return count;
|
|
198
265
|
});
|
|
199
266
|
}
|
|
200
267
|
|
|
201
268
|
clearAll(): number {
|
|
202
269
|
return this.withLock(() => {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
270
|
+
const ids = [...this.loops.keys()];
|
|
271
|
+
for (const id of ids) {
|
|
272
|
+
this.applyReducerEvent({
|
|
273
|
+
type: "LOOP_DELETED",
|
|
274
|
+
at: Date.now(),
|
|
275
|
+
source: "system",
|
|
276
|
+
entityType: "loop",
|
|
277
|
+
entityId: id,
|
|
278
|
+
payload: { id },
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return ids.length;
|
|
206
282
|
});
|
|
207
283
|
}
|
|
208
284
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ReducerEffect, ReducerEvent } from "./coordinator.js";
|
|
2
|
+
|
|
3
|
+
export type TaskBacklogEvent = ReducerEvent<
|
|
4
|
+
"TASK_BACKLOG_EVALUATED",
|
|
5
|
+
{ pendingCount: number; threshold: number }
|
|
6
|
+
>;
|
|
7
|
+
|
|
8
|
+
export type TaskBacklogEffect =
|
|
9
|
+
| ReducerEffect<"ENSURE_AUTO_TASK_WORKER", { pendingCount: number; threshold: number }>
|
|
10
|
+
| ReducerEffect<"CLEANUP_TASK_BACKLOG_LOOPS", { pendingCount: number }>;
|
|
11
|
+
|
|
12
|
+
export function reduceTaskBacklogEvent(event: TaskBacklogEvent): TaskBacklogEffect[] {
|
|
13
|
+
if (event.type !== "TASK_BACKLOG_EVALUATED") return [];
|
|
14
|
+
|
|
15
|
+
const { pendingCount, threshold } = event.payload;
|
|
16
|
+
if (pendingCount < 0) return [];
|
|
17
|
+
if (pendingCount === 0) {
|
|
18
|
+
return [{
|
|
19
|
+
type: "CLEANUP_TASK_BACKLOG_LOOPS",
|
|
20
|
+
entityType: "task",
|
|
21
|
+
payload: { pendingCount },
|
|
22
|
+
}];
|
|
23
|
+
}
|
|
24
|
+
if (pendingCount >= threshold) {
|
|
25
|
+
return [{
|
|
26
|
+
type: "ENSURE_AUTO_TASK_WORKER",
|
|
27
|
+
entityType: "task",
|
|
28
|
+
payload: { pendingCount, threshold },
|
|
29
|
+
}];
|
|
30
|
+
}
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { TaskEntry } from "./task-types.js";
|
|
2
|
+
|
|
3
|
+
export interface TaskReducerState {
|
|
4
|
+
nextId: number;
|
|
5
|
+
tasksById: Record<string, TaskEntry>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type TaskReducerEvent =
|
|
9
|
+
| {
|
|
10
|
+
type: "TASK_CREATED";
|
|
11
|
+
at: number;
|
|
12
|
+
source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
13
|
+
entityType?: "task";
|
|
14
|
+
entityId?: string;
|
|
15
|
+
payload: {
|
|
16
|
+
subject: string;
|
|
17
|
+
description: string;
|
|
18
|
+
metadata?: Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
type: "TASK_STARTED" | "TASK_COMPLETED" | "TASK_REOPENED" | "TASK_DELETED";
|
|
23
|
+
at: number;
|
|
24
|
+
source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
25
|
+
entityType?: "task";
|
|
26
|
+
entityId?: string;
|
|
27
|
+
payload: { id: string };
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
type: "TASK_UPDATED";
|
|
31
|
+
at: number;
|
|
32
|
+
source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
33
|
+
entityType?: "task";
|
|
34
|
+
entityId?: string;
|
|
35
|
+
payload: {
|
|
36
|
+
id: string;
|
|
37
|
+
subject?: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
type: "TASKS_PRUNED";
|
|
43
|
+
at: number;
|
|
44
|
+
source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
45
|
+
entityType?: "task";
|
|
46
|
+
entityId?: string;
|
|
47
|
+
payload: {
|
|
48
|
+
reason: "git_commit" | "zero_pending_cleanup" | "manual";
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type TaskReducerEffect =
|
|
53
|
+
| {
|
|
54
|
+
type: "PERSIST_TASK";
|
|
55
|
+
entityType: "task";
|
|
56
|
+
entityId: string;
|
|
57
|
+
payload: { task: TaskEntry };
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
type: "DELETE_TASK";
|
|
61
|
+
entityType: "task";
|
|
62
|
+
entityId: string;
|
|
63
|
+
payload: { id: string };
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export interface TaskReduceResult {
|
|
67
|
+
state: TaskReducerState;
|
|
68
|
+
effects: TaskReducerEffect[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cloneState(state: TaskReducerState): TaskReducerState {
|
|
72
|
+
return {
|
|
73
|
+
nextId: state.nextId,
|
|
74
|
+
tasksById: { ...state.tasksById },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function reduceTaskState(state: TaskReducerState, event: TaskReducerEvent): TaskReduceResult {
|
|
79
|
+
if (event.type === "TASK_CREATED") {
|
|
80
|
+
const next = cloneState(state);
|
|
81
|
+
const id = String(next.nextId++);
|
|
82
|
+
const task: TaskEntry = {
|
|
83
|
+
id,
|
|
84
|
+
subject: event.payload.subject,
|
|
85
|
+
description: event.payload.description,
|
|
86
|
+
status: "pending",
|
|
87
|
+
createdAt: event.at,
|
|
88
|
+
updatedAt: event.at,
|
|
89
|
+
metadata: event.payload.metadata,
|
|
90
|
+
};
|
|
91
|
+
next.tasksById[id] = task;
|
|
92
|
+
return {
|
|
93
|
+
state: next,
|
|
94
|
+
effects: [{ type: "PERSIST_TASK", entityType: "task", entityId: id, payload: { task } }],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (event.type === "TASKS_PRUNED") {
|
|
99
|
+
const next = cloneState(state);
|
|
100
|
+
const effects: TaskReducerEffect[] = [];
|
|
101
|
+
for (const [id, task] of Object.entries(next.tasksById)) {
|
|
102
|
+
if (task.status !== "completed") continue;
|
|
103
|
+
delete next.tasksById[id];
|
|
104
|
+
effects.push({ type: "DELETE_TASK", entityType: "task", entityId: id, payload: { id } });
|
|
105
|
+
}
|
|
106
|
+
return { state: next, effects };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const id = event.payload.id;
|
|
110
|
+
const current = state.tasksById[id];
|
|
111
|
+
if (!current) return { state, effects: [] };
|
|
112
|
+
|
|
113
|
+
if (event.type === "TASK_DELETED") {
|
|
114
|
+
const next = cloneState(state);
|
|
115
|
+
delete next.tasksById[id];
|
|
116
|
+
return {
|
|
117
|
+
state: next,
|
|
118
|
+
effects: [{ type: "DELETE_TASK", entityType: "task", entityId: id, payload: { id } }],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const next = cloneState(state);
|
|
123
|
+
const task: TaskEntry = { ...current };
|
|
124
|
+
|
|
125
|
+
if (event.type === "TASK_STARTED") {
|
|
126
|
+
task.status = "in_progress";
|
|
127
|
+
task.updatedAt = event.at;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (event.type === "TASK_COMPLETED") {
|
|
131
|
+
task.status = "completed";
|
|
132
|
+
task.updatedAt = event.at;
|
|
133
|
+
task.completedAt = event.at;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (event.type === "TASK_REOPENED") {
|
|
137
|
+
task.status = "pending";
|
|
138
|
+
task.updatedAt = event.at;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (event.type === "TASK_UPDATED") {
|
|
142
|
+
if (event.payload.subject !== undefined) task.subject = event.payload.subject;
|
|
143
|
+
if (event.payload.description !== undefined) task.description = event.payload.description;
|
|
144
|
+
task.updatedAt = event.at;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
next.tasksById[id] = task;
|
|
148
|
+
return {
|
|
149
|
+
state: next,
|
|
150
|
+
effects: [{ type: "PERSIST_TASK", entityType: "task", entityId: id, payload: { task } }],
|
|
151
|
+
};
|
|
152
|
+
}
|
package/src/task-store.ts
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, type TaskReducerEvent, type TaskReducerState } from "./task-reducer.js";
|
|
4
5
|
import type { TaskEntry, TaskStatus, TaskStoreData } from "./task-types.js";
|
|
5
6
|
|
|
6
7
|
const TASKS_DIR = join(homedir(), ".pi", "tasks");
|
|
@@ -94,23 +95,33 @@ export class TaskStore {
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
private toReducerState(): TaskReducerState {
|
|
99
|
+
return {
|
|
100
|
+
nextId: this.nextId,
|
|
101
|
+
tasksById: Object.fromEntries(this.tasks.entries()),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private applyReducerEvent(event: TaskReducerEvent): void {
|
|
106
|
+
const result = reduceTaskState(this.toReducerState(), event);
|
|
107
|
+
this.nextId = result.state.nextId;
|
|
108
|
+
this.tasks = new Map(Object.entries(result.state.tasksById));
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
create(subject: string, description: string, metadata?: Record<string, unknown>): TaskEntry {
|
|
98
112
|
return this.withLock(() => {
|
|
99
113
|
if (this.tasks.size >= MAX_TASKS) {
|
|
100
114
|
throw new Error(`Maximum of ${MAX_TASKS} tasks reached. Delete some before creating new ones.`);
|
|
101
115
|
}
|
|
102
116
|
const now = Date.now();
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
};
|
|
112
|
-
this.tasks.set(entry.id, entry);
|
|
113
|
-
return entry;
|
|
117
|
+
this.applyReducerEvent({
|
|
118
|
+
type: "TASK_CREATED",
|
|
119
|
+
at: now,
|
|
120
|
+
source: "tool",
|
|
121
|
+
entityType: "task",
|
|
122
|
+
payload: { subject, description, metadata },
|
|
123
|
+
});
|
|
124
|
+
return this.tasks.get(String(this.nextId - 1))!;
|
|
114
125
|
});
|
|
115
126
|
}
|
|
116
127
|
|
|
@@ -129,21 +140,66 @@ export class TaskStore {
|
|
|
129
140
|
const entry = this.tasks.get(id);
|
|
130
141
|
if (!entry) return undefined;
|
|
131
142
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
if (fields.status === "in_progress") {
|
|
145
|
+
this.applyReducerEvent({
|
|
146
|
+
type: "TASK_STARTED",
|
|
147
|
+
at: now,
|
|
148
|
+
source: "tool",
|
|
149
|
+
entityType: "task",
|
|
150
|
+
entityId: id,
|
|
151
|
+
payload: { id },
|
|
152
|
+
});
|
|
153
|
+
} else if (fields.status === "completed") {
|
|
154
|
+
this.applyReducerEvent({
|
|
155
|
+
type: "TASK_COMPLETED",
|
|
156
|
+
at: now,
|
|
157
|
+
source: "tool",
|
|
158
|
+
entityType: "task",
|
|
159
|
+
entityId: id,
|
|
160
|
+
payload: { id },
|
|
161
|
+
});
|
|
162
|
+
} else if (fields.status === "pending") {
|
|
163
|
+
this.applyReducerEvent({
|
|
164
|
+
type: "TASK_REOPENED",
|
|
165
|
+
at: now,
|
|
166
|
+
source: "tool",
|
|
167
|
+
entityType: "task",
|
|
168
|
+
entityId: id,
|
|
169
|
+
payload: { id },
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (fields.subject !== undefined || fields.description !== undefined) {
|
|
174
|
+
this.applyReducerEvent({
|
|
175
|
+
type: "TASK_UPDATED",
|
|
176
|
+
at: now,
|
|
177
|
+
source: "tool",
|
|
178
|
+
entityType: "task",
|
|
179
|
+
entityId: id,
|
|
180
|
+
payload: {
|
|
181
|
+
id,
|
|
182
|
+
subject: fields.subject,
|
|
183
|
+
description: fields.description,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
135
186
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
entry.updatedAt = Date.now();
|
|
139
|
-
return entry;
|
|
187
|
+
|
|
188
|
+
return this.tasks.get(id);
|
|
140
189
|
});
|
|
141
190
|
}
|
|
142
191
|
|
|
143
192
|
delete(id: string): boolean {
|
|
144
193
|
return this.withLock(() => {
|
|
145
194
|
if (!this.tasks.has(id)) return false;
|
|
146
|
-
this.
|
|
195
|
+
this.applyReducerEvent({
|
|
196
|
+
type: "TASK_DELETED",
|
|
197
|
+
at: Date.now(),
|
|
198
|
+
source: "tool",
|
|
199
|
+
entityType: "task",
|
|
200
|
+
entityId: id,
|
|
201
|
+
payload: { id },
|
|
202
|
+
});
|
|
147
203
|
return true;
|
|
148
204
|
});
|
|
149
205
|
}
|
|
@@ -158,14 +214,15 @@ export class TaskStore {
|
|
|
158
214
|
|
|
159
215
|
sweepCompleted(): number {
|
|
160
216
|
return this.withLock(() => {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
217
|
+
const before = this.tasks.size;
|
|
218
|
+
this.applyReducerEvent({
|
|
219
|
+
type: "TASKS_PRUNED",
|
|
220
|
+
at: Date.now(),
|
|
221
|
+
source: "system",
|
|
222
|
+
entityType: "task",
|
|
223
|
+
payload: { reason: "manual" },
|
|
224
|
+
});
|
|
225
|
+
return before - this.tasks.size;
|
|
169
226
|
});
|
|
170
227
|
}
|
|
171
228
|
}
|