@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,150 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { TaskStore } from "../task-store.js";
|
|
4
|
+
import type { LoopEntry } from "../types.js";
|
|
5
|
+
|
|
6
|
+
export interface TaskRuntimeBridgeOptions {
|
|
7
|
+
pi: ExtensionAPI;
|
|
8
|
+
isTasksAvailable: () => boolean;
|
|
9
|
+
setTasksAvailable: (available: boolean) => void;
|
|
10
|
+
getNativeTaskStore: () => TaskStore | undefined;
|
|
11
|
+
onNativeTaskCreated?: (taskStore: TaskStore) => void;
|
|
12
|
+
onNativeTasksPruned?: (taskStore: TaskStore) => Promise<void> | void;
|
|
13
|
+
debug?: (...args: unknown[]) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TaskRuntimeBridge {
|
|
17
|
+
checkTasksVersion(): void;
|
|
18
|
+
autoCreateTask(entry: LoopEntry): Promise<string | undefined>;
|
|
19
|
+
hasPendingTasks(): Promise<number>;
|
|
20
|
+
cleanDoneTasks(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createTaskRuntimeBridge(options: TaskRuntimeBridgeOptions): TaskRuntimeBridge {
|
|
24
|
+
const {
|
|
25
|
+
pi,
|
|
26
|
+
isTasksAvailable,
|
|
27
|
+
setTasksAvailable,
|
|
28
|
+
getNativeTaskStore,
|
|
29
|
+
onNativeTaskCreated,
|
|
30
|
+
onNativeTasksPruned,
|
|
31
|
+
debug,
|
|
32
|
+
} = options;
|
|
33
|
+
|
|
34
|
+
function checkTasksVersion() {
|
|
35
|
+
const requestId = randomUUID();
|
|
36
|
+
const timer = setTimeout(() => {
|
|
37
|
+
unsub();
|
|
38
|
+
}, 5000);
|
|
39
|
+
const unsub = pi.events.on(`tasks:rpc:ping:reply:${requestId}`, (raw: unknown) => {
|
|
40
|
+
unsub();
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
const remoteVersion = (raw as { data?: { version?: number } })?.data?.version;
|
|
43
|
+
if (remoteVersion !== undefined) setTasksAvailable(true);
|
|
44
|
+
});
|
|
45
|
+
pi.events.emit("tasks:rpc:ping", { requestId });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function autoCreateTask(entry: LoopEntry): Promise<string | undefined> {
|
|
49
|
+
if (!entry.autoTask) return undefined;
|
|
50
|
+
if (isTasksAvailable()) {
|
|
51
|
+
try {
|
|
52
|
+
const requestId = randomUUID();
|
|
53
|
+
const taskId = await new Promise<string | undefined>((resolve) => {
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
unsub();
|
|
56
|
+
resolve(undefined);
|
|
57
|
+
}, 5000);
|
|
58
|
+
const unsub = pi.events.on(`tasks:rpc:create:reply:${requestId}`, (raw: unknown) => {
|
|
59
|
+
unsub();
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
const reply = raw as { success: boolean; data?: { id: string } };
|
|
62
|
+
if (reply.success && reply.data) resolve(reply.data.id);
|
|
63
|
+
else resolve(undefined);
|
|
64
|
+
});
|
|
65
|
+
pi.events.emit("tasks:rpc:create", {
|
|
66
|
+
requestId,
|
|
67
|
+
subject: entry.prompt.slice(0, 80),
|
|
68
|
+
description: `Auto-created from loop #${entry.id}`,
|
|
69
|
+
metadata: { loopId: entry.id, trigger: entry.trigger },
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
return taskId;
|
|
73
|
+
} catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const nativeTaskStore = getNativeTaskStore();
|
|
79
|
+
if (!nativeTaskStore) return undefined;
|
|
80
|
+
const task = nativeTaskStore.create(
|
|
81
|
+
entry.prompt.slice(0, 80),
|
|
82
|
+
`Auto-created from loop #${entry.id}`,
|
|
83
|
+
{ loopId: entry.id, trigger: entry.trigger },
|
|
84
|
+
);
|
|
85
|
+
onNativeTaskCreated?.(nativeTaskStore);
|
|
86
|
+
return task.id;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function hasPendingTasks(): Promise<number> {
|
|
90
|
+
if (isTasksAvailable()) {
|
|
91
|
+
try {
|
|
92
|
+
const requestId = randomUUID();
|
|
93
|
+
const count = await new Promise<number>((resolve) => {
|
|
94
|
+
const timer = setTimeout(() => {
|
|
95
|
+
unsub();
|
|
96
|
+
resolve(-1);
|
|
97
|
+
}, 3000);
|
|
98
|
+
const unsub = pi.events.on(`tasks:rpc:pending:reply:${requestId}`, (raw: unknown) => {
|
|
99
|
+
unsub();
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
const reply = raw as { success: boolean; data?: { pending: number } };
|
|
102
|
+
resolve(reply.success && reply.data ? reply.data.pending : -1);
|
|
103
|
+
});
|
|
104
|
+
pi.events.emit("tasks:rpc:pending", { requestId });
|
|
105
|
+
});
|
|
106
|
+
return count;
|
|
107
|
+
} catch {
|
|
108
|
+
return -1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return getNativeTaskStore()?.pendingCount() ?? -1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function cleanDoneTasks(): Promise<void> {
|
|
116
|
+
if (isTasksAvailable()) {
|
|
117
|
+
try {
|
|
118
|
+
const requestId = randomUUID();
|
|
119
|
+
await new Promise<void>((resolve) => {
|
|
120
|
+
const timer = setTimeout(() => {
|
|
121
|
+
unsub();
|
|
122
|
+
resolve();
|
|
123
|
+
}, 3000);
|
|
124
|
+
const unsub = pi.events.on(`tasks:rpc:clean:reply:${requestId}`, () => {
|
|
125
|
+
unsub();
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
debug?.("tasks:rpc:clean — done tasks swept");
|
|
128
|
+
resolve();
|
|
129
|
+
});
|
|
130
|
+
pi.events.emit("tasks:rpc:clean", { requestId });
|
|
131
|
+
});
|
|
132
|
+
} catch {
|
|
133
|
+
// timeout or error, ignore
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const nativeTaskStore = getNativeTaskStore();
|
|
139
|
+
if (!nativeTaskStore) return;
|
|
140
|
+
nativeTaskStore.pruneCompleted();
|
|
141
|
+
await onNativeTasksPruned?.(nativeTaskStore);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
checkTasksVersion,
|
|
146
|
+
autoCreateTask,
|
|
147
|
+
hasPendingTasks,
|
|
148
|
+
cleanDoneTasks,
|
|
149
|
+
};
|
|
150
|
+
}
|
package/src/store.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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 { type LoopReducerEvent, type LoopReducerState, reduceLoopState } from "./loop-reducer.js";
|
|
5
|
-
import type { LoopEntry,
|
|
5
|
+
import type { LoopEntry, LoopStoreData, Trigger } from "./types.js";
|
|
6
6
|
|
|
7
7
|
const LOOPS_DIR = join(homedir(), ".pi", "loops");
|
|
8
8
|
const LOCK_RETRY_MS = 50;
|
|
@@ -44,6 +44,7 @@ function isProcessRunning(pid: number): boolean {
|
|
|
44
44
|
export class LoopStore {
|
|
45
45
|
private filePath: string | undefined;
|
|
46
46
|
private lockPath: string | undefined;
|
|
47
|
+
private lastLoadedSignature: string | undefined;
|
|
47
48
|
|
|
48
49
|
private nextId = 1;
|
|
49
50
|
private loops = new Map<string, LoopEntry>();
|
|
@@ -58,9 +59,17 @@ export class LoopStore {
|
|
|
58
59
|
this.load();
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
private
|
|
62
|
+
private getFileSignature(): string | undefined {
|
|
63
|
+
if (!this.filePath || !existsSync(this.filePath)) return undefined;
|
|
64
|
+
const stat = statSync(this.filePath);
|
|
65
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private load(force = false): void {
|
|
62
69
|
if (!this.filePath) return;
|
|
63
|
-
|
|
70
|
+
const signature = this.getFileSignature();
|
|
71
|
+
if (!signature) return;
|
|
72
|
+
if (!force && signature === this.lastLoadedSignature) return;
|
|
64
73
|
try {
|
|
65
74
|
const data: LoopStoreData = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
66
75
|
this.nextId = data.nextId;
|
|
@@ -68,6 +77,7 @@ export class LoopStore {
|
|
|
68
77
|
for (const loop of data.loops) {
|
|
69
78
|
this.loops.set(loop.id, loop);
|
|
70
79
|
}
|
|
80
|
+
this.lastLoadedSignature = signature;
|
|
71
81
|
} catch { /* corrupt file — start fresh */ }
|
|
72
82
|
}
|
|
73
83
|
|
|
@@ -80,13 +90,14 @@ export class LoopStore {
|
|
|
80
90
|
const tmpPath = this.filePath + ".tmp";
|
|
81
91
|
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
82
92
|
renameSync(tmpPath, this.filePath);
|
|
93
|
+
this.lastLoadedSignature = this.getFileSignature();
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
private withLock<T>(fn: () => T): T {
|
|
86
97
|
if (!this.lockPath) return fn();
|
|
87
98
|
acquireLock(this.lockPath);
|
|
88
99
|
try {
|
|
89
|
-
this.load();
|
|
100
|
+
this.load(true);
|
|
90
101
|
const result = fn();
|
|
91
102
|
this.save();
|
|
92
103
|
return result;
|
|
@@ -143,38 +154,61 @@ export class LoopStore {
|
|
|
143
154
|
return Array.from(this.loops.values()).sort((a, b) => Number(a.id) - Number(b.id));
|
|
144
155
|
}
|
|
145
156
|
|
|
146
|
-
|
|
157
|
+
pause(id: string): LoopEntry | undefined {
|
|
147
158
|
return this.withLock(() => {
|
|
148
159
|
const entry = this.loops.get(id);
|
|
149
|
-
if (!entry) return
|
|
160
|
+
if (!entry) return undefined;
|
|
161
|
+
this.applyReducerEvent({
|
|
162
|
+
type: "LOOP_PAUSED",
|
|
163
|
+
at: Date.now(),
|
|
164
|
+
source: "tool",
|
|
165
|
+
entityType: "loop",
|
|
166
|
+
entityId: id,
|
|
167
|
+
payload: { id },
|
|
168
|
+
});
|
|
169
|
+
return this.loops.get(id);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
150
172
|
|
|
151
|
-
|
|
152
|
-
|
|
173
|
+
resume(id: string): LoopEntry | undefined {
|
|
174
|
+
return this.withLock(() => {
|
|
175
|
+
const entry = this.loops.get(id);
|
|
176
|
+
if (!entry) return undefined;
|
|
177
|
+
this.applyReducerEvent({
|
|
178
|
+
type: "LOOP_RESUMED",
|
|
179
|
+
at: Date.now(),
|
|
180
|
+
source: "tool",
|
|
181
|
+
entityType: "loop",
|
|
182
|
+
entityId: id,
|
|
183
|
+
payload: { id },
|
|
184
|
+
});
|
|
185
|
+
return this.loops.get(id);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
153
188
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
entityType: "loop",
|
|
170
|
-
entityId: id,
|
|
171
|
-
payload: { id },
|
|
172
|
-
});
|
|
173
|
-
changedFields.push("status");
|
|
174
|
-
}
|
|
189
|
+
fire(id: string): LoopEntry | undefined {
|
|
190
|
+
return this.withLock(() => {
|
|
191
|
+
const entry = this.loops.get(id);
|
|
192
|
+
if (!entry) return undefined;
|
|
193
|
+
this.applyReducerEvent({
|
|
194
|
+
type: "LOOP_FIRED",
|
|
195
|
+
at: Date.now(),
|
|
196
|
+
source: "system",
|
|
197
|
+
entityType: "loop",
|
|
198
|
+
entityId: id,
|
|
199
|
+
payload: { id },
|
|
200
|
+
});
|
|
201
|
+
return this.loops.get(id);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
175
204
|
|
|
205
|
+
updateMetadata(id: string, fields: { trigger?: Trigger; prompt?: string }): { entry: LoopEntry | undefined; changedFields: string[] } {
|
|
206
|
+
return this.withLock(() => {
|
|
176
207
|
const current = this.loops.get(id);
|
|
177
|
-
if (!current) return { entry: undefined, changedFields };
|
|
208
|
+
if (!current) return { entry: undefined, changedFields: [] };
|
|
209
|
+
|
|
210
|
+
const changedFields: string[] = [];
|
|
211
|
+
const now = Date.now();
|
|
178
212
|
|
|
179
213
|
if (fields.trigger !== undefined) {
|
|
180
214
|
current.trigger = fields.trigger;
|
|
@@ -184,24 +218,7 @@ export class LoopStore {
|
|
|
184
218
|
current.prompt = fields.prompt;
|
|
185
219
|
changedFields.push("prompt");
|
|
186
220
|
}
|
|
187
|
-
if (
|
|
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
|
-
}
|
|
201
|
-
changedFields.push("fireCount");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (fields.trigger !== undefined || fields.prompt !== undefined) {
|
|
221
|
+
if (changedFields.length > 0) {
|
|
205
222
|
current.updatedAt = now;
|
|
206
223
|
}
|
|
207
224
|
|
package/src/task-store.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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, type TaskReducerEvent, type TaskReducerState } from "./task-reducer.js";
|
|
5
|
-
import type { TaskEntry,
|
|
5
|
+
import type { TaskEntry, TaskStoreData } from "./task-types.js";
|
|
6
6
|
|
|
7
7
|
const TASKS_DIR = join(homedir(), ".pi", "tasks");
|
|
8
8
|
const LOCK_RETRY_MS = 50;
|
|
@@ -44,6 +44,7 @@ function isProcessRunning(pid: number): boolean {
|
|
|
44
44
|
export class TaskStore {
|
|
45
45
|
private filePath: string | undefined;
|
|
46
46
|
private lockPath: string | undefined;
|
|
47
|
+
private lastLoadedSignature: string | undefined;
|
|
47
48
|
|
|
48
49
|
private nextId = 1;
|
|
49
50
|
private tasks = new Map<string, TaskEntry>();
|
|
@@ -58,9 +59,17 @@ export class TaskStore {
|
|
|
58
59
|
this.load();
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
private
|
|
62
|
+
private getFileSignature(): string | undefined {
|
|
63
|
+
if (!this.filePath || !existsSync(this.filePath)) return undefined;
|
|
64
|
+
const stat = statSync(this.filePath);
|
|
65
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private load(force = false): void {
|
|
62
69
|
if (!this.filePath) return;
|
|
63
|
-
|
|
70
|
+
const signature = this.getFileSignature();
|
|
71
|
+
if (!signature) return;
|
|
72
|
+
if (!force && signature === this.lastLoadedSignature) return;
|
|
64
73
|
try {
|
|
65
74
|
const data: TaskStoreData = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
66
75
|
this.nextId = data.nextId;
|
|
@@ -68,6 +77,7 @@ export class TaskStore {
|
|
|
68
77
|
for (const task of data.tasks) {
|
|
69
78
|
this.tasks.set(task.id, task);
|
|
70
79
|
}
|
|
80
|
+
this.lastLoadedSignature = signature;
|
|
71
81
|
} catch { /* corrupt file — start fresh */ }
|
|
72
82
|
}
|
|
73
83
|
|
|
@@ -80,13 +90,14 @@ export class TaskStore {
|
|
|
80
90
|
const tmpPath = this.filePath + ".tmp";
|
|
81
91
|
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
82
92
|
renameSync(tmpPath, this.filePath);
|
|
93
|
+
this.lastLoadedSignature = this.getFileSignature();
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
private withLock<T>(fn: () => T): T {
|
|
86
97
|
if (!this.lockPath) return fn();
|
|
87
98
|
acquireLock(this.lockPath);
|
|
88
99
|
try {
|
|
89
|
-
this.load();
|
|
100
|
+
this.load(true);
|
|
90
101
|
const result = fn();
|
|
91
102
|
this.save();
|
|
92
103
|
return result;
|
|
@@ -135,56 +146,71 @@ export class TaskStore {
|
|
|
135
146
|
return Array.from(this.tasks.values()).sort((a, b) => Number(a.id) - Number(b.id));
|
|
136
147
|
}
|
|
137
148
|
|
|
138
|
-
|
|
149
|
+
start(id: string): TaskEntry | undefined {
|
|
139
150
|
return this.withLock(() => {
|
|
140
151
|
const entry = this.tasks.get(id);
|
|
141
152
|
if (!entry) return undefined;
|
|
153
|
+
this.applyReducerEvent({
|
|
154
|
+
type: "TASK_STARTED",
|
|
155
|
+
at: Date.now(),
|
|
156
|
+
source: "tool",
|
|
157
|
+
entityType: "task",
|
|
158
|
+
entityId: id,
|
|
159
|
+
payload: { id },
|
|
160
|
+
});
|
|
161
|
+
return this.tasks.get(id);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
142
164
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
}
|
|
165
|
+
complete(id: string): TaskEntry | undefined {
|
|
166
|
+
return this.withLock(() => {
|
|
167
|
+
const entry = this.tasks.get(id);
|
|
168
|
+
if (!entry) return undefined;
|
|
169
|
+
this.applyReducerEvent({
|
|
170
|
+
type: "TASK_COMPLETED",
|
|
171
|
+
at: Date.now(),
|
|
172
|
+
source: "tool",
|
|
173
|
+
entityType: "task",
|
|
174
|
+
entityId: id,
|
|
175
|
+
payload: { id },
|
|
176
|
+
});
|
|
177
|
+
return this.tasks.get(id);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
172
180
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
181
|
+
reopen(id: string): TaskEntry | undefined {
|
|
182
|
+
return this.withLock(() => {
|
|
183
|
+
const entry = this.tasks.get(id);
|
|
184
|
+
if (!entry) return undefined;
|
|
185
|
+
this.applyReducerEvent({
|
|
186
|
+
type: "TASK_REOPENED",
|
|
187
|
+
at: Date.now(),
|
|
188
|
+
source: "tool",
|
|
189
|
+
entityType: "task",
|
|
190
|
+
entityId: id,
|
|
191
|
+
payload: { id },
|
|
192
|
+
});
|
|
193
|
+
return this.tasks.get(id);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
187
196
|
|
|
197
|
+
updateDetails(id: string, fields: { subject?: string; description?: string }): TaskEntry | undefined {
|
|
198
|
+
return this.withLock(() => {
|
|
199
|
+
const entry = this.tasks.get(id);
|
|
200
|
+
if (!entry) return undefined;
|
|
201
|
+
if (fields.subject === undefined && fields.description === undefined) return entry;
|
|
202
|
+
this.applyReducerEvent({
|
|
203
|
+
type: "TASK_UPDATED",
|
|
204
|
+
at: Date.now(),
|
|
205
|
+
source: "tool",
|
|
206
|
+
entityType: "task",
|
|
207
|
+
entityId: id,
|
|
208
|
+
payload: {
|
|
209
|
+
id,
|
|
210
|
+
subject: fields.subject,
|
|
211
|
+
description: fields.description,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
188
214
|
return this.tasks.get(id);
|
|
189
215
|
});
|
|
190
216
|
}
|
|
@@ -212,7 +238,7 @@ export class TaskStore {
|
|
|
212
238
|
return count;
|
|
213
239
|
}
|
|
214
240
|
|
|
215
|
-
|
|
241
|
+
pruneCompleted(): number {
|
|
216
242
|
return this.withLock(() => {
|
|
217
243
|
const before = this.tasks.size;
|
|
218
244
|
this.applyReducerEvent({
|