@trevonistrevon/pi-loop 0.5.3 → 0.5.5
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/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-final.json +32 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +176 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/commands/index.html +131 -0
- package/coverage/src/commands/loop-command.ts.html +634 -0
- package/coverage/src/commands/tasks-command.ts.html +490 -0
- package/coverage/src/coordinator.ts.html +430 -0
- package/coverage/src/goal-coordinator.ts.html +259 -0
- package/coverage/src/goal-reducer.ts.html +982 -0
- package/coverage/src/goal-store.ts.html +742 -0
- package/coverage/src/goal-verifier.ts.html +808 -0
- package/coverage/src/index.html +386 -0
- package/coverage/src/index.ts.html +1054 -0
- package/coverage/src/loop-parse.ts.html +574 -0
- package/coverage/src/loop-reducer.ts.html +559 -0
- package/coverage/src/monitor-completion-coordinator.ts.html +157 -0
- package/coverage/src/monitor-manager.ts.html +865 -0
- package/coverage/src/monitor-reducer.ts.html +583 -0
- package/coverage/src/notification-reducer.ts.html +550 -0
- package/coverage/src/reducer-backed-store.ts.html +589 -0
- package/coverage/src/runtime/index.html +191 -0
- package/coverage/src/runtime/monitor-ondone-runtime.ts.html +310 -0
- package/coverage/src/runtime/notification-runtime.ts.html +721 -0
- package/coverage/src/runtime/scope.ts.html +196 -0
- package/coverage/src/runtime/session-runtime.ts.html +589 -0
- package/coverage/src/runtime/task-backlog-runtime.ts.html +574 -0
- package/coverage/src/runtime/task-rpc.ts.html +535 -0
- package/coverage/src/scheduler.ts.html +400 -0
- package/coverage/src/store.ts.html +667 -0
- package/coverage/src/task-backlog-coordinator.ts.html +181 -0
- package/coverage/src/task-reducer.ts.html +550 -0
- package/coverage/src/task-store.ts.html +526 -0
- package/coverage/src/tools/index.html +146 -0
- package/coverage/src/tools/loop-tools.ts.html +1000 -0
- package/coverage/src/tools/monitor-tools.ts.html +547 -0
- package/coverage/src/tools/native-task-tools.ts.html +535 -0
- package/coverage/src/trigger-system.ts.html +547 -0
- package/coverage/src/ui/index.html +116 -0
- package/coverage/src/ui/widget.ts.html +292 -0
- package/dist/commands/loop-command.js +4 -5
- package/dist/commands/tasks-command.js +3 -3
- package/dist/goal-reducer.d.ts +9 -0
- package/dist/goal-reducer.js +11 -2
- package/dist/goal-store.d.ts +4 -15
- package/dist/goal-store.js +32 -154
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -3
- package/dist/loop-reducer.d.ts +7 -0
- package/dist/loop-reducer.js +9 -0
- package/dist/monitor-manager.js +4 -4
- package/dist/reducer-backed-store.d.ts +65 -0
- package/dist/reducer-backed-store.js +160 -0
- package/dist/store.d.ts +4 -16
- package/dist/store.js +25 -156
- package/dist/task-reducer.js +3 -0
- package/dist/task-store.d.ts +4 -15
- package/dist/task-store.js +25 -147
- package/dist/tools/loop-tools.js +7 -5
- package/dist/tools/monitor-tools.js +12 -3
- package/dist/tools/native-task-tools.js +2 -2
- package/dist/trigger-system.js +2 -1
- package/dist/ui/widget.js +8 -1
- package/package.json +4 -1
- package/src/commands/loop-command.ts +4 -5
- package/src/commands/tasks-command.ts +3 -3
- package/src/goal-reducer.ts +20 -1
- package/src/goal-store.ts +35 -142
- package/src/index.ts +4 -3
- package/src/loop-reducer.ts +10 -0
- package/src/monitor-manager.ts +2 -3
- package/src/reducer-backed-store.ts +168 -0
- package/src/store.ts +28 -141
- package/src/task-reducer.ts +3 -0
- package/src/task-store.ts +28 -135
- package/src/tools/loop-tools.ts +6 -5
- package/src/tools/monitor-tools.ts +12 -3
- package/src/tools/native-task-tools.ts +2 -2
- package/src/trigger-system.ts +2 -1
- package/src/ui/widget.ts +8 -1
- package/vitest.config.ts +25 -0
- package/DIFFERENTIAL_REVIEW_REPORT.md +0 -81
package/dist/ui/widget.js
CHANGED
|
@@ -22,7 +22,7 @@ export class LoopWidget {
|
|
|
22
22
|
this.uiCtx.setStatus("loops", this.computeStatus());
|
|
23
23
|
}
|
|
24
24
|
computeStatus() {
|
|
25
|
-
const loops = this.store.list().filter(
|
|
25
|
+
const loops = this.store.list().filter(isStatusVisibleLoop);
|
|
26
26
|
const monitors = this.monitorManager.list();
|
|
27
27
|
const taskSummary = this.taskSummaryProvider?.() ?? { count: 0 };
|
|
28
28
|
if (loops.length === 0 && monitors.length === 0 && taskSummary.count === 0) {
|
|
@@ -47,3 +47,10 @@ export class LoopWidget {
|
|
|
47
47
|
function formatCount(count, noun) {
|
|
48
48
|
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
49
49
|
}
|
|
50
|
+
function isStatusVisibleLoop(loop) {
|
|
51
|
+
if (loop.status !== "active")
|
|
52
|
+
return false;
|
|
53
|
+
if (loop.recurring)
|
|
54
|
+
return true;
|
|
55
|
+
return !(loop.trigger.type === "event" && loop.trigger.source === "monitor:done");
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trevonistrevon/pi-loop",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "A pi extension for cron/event-based agent re-wake loops and background process monitoring.",
|
|
5
5
|
"author": "trevonistrevon",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"build": "tsc",
|
|
29
29
|
"prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build",
|
|
30
30
|
"test": "vitest run",
|
|
31
|
+
"test:coverage": "vitest run --coverage",
|
|
31
32
|
"test:watch": "vitest",
|
|
33
|
+
"test:e2e": "bash test/e2e/reminder-injection.sh",
|
|
32
34
|
"typecheck": "tsc --noEmit",
|
|
33
35
|
"lint": "biome check src/ test/",
|
|
34
36
|
"lint:fix": "biome check --fix src/ test/"
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
"devDependencies": {
|
|
37
39
|
"@biomejs/biome": "^2.4.13",
|
|
38
40
|
"@types/node": "^25.0.0",
|
|
41
|
+
"@vitest/coverage-v8": "^4.1.8",
|
|
39
42
|
"typescript": "^6.0.0",
|
|
40
43
|
"vitest": "^4.1.5"
|
|
41
44
|
},
|
|
@@ -66,7 +66,7 @@ export function registerLoopCommand(options: LoopCommandOptions): void {
|
|
|
66
66
|
async function viewLoops(ui: ExtensionUIContext) {
|
|
67
67
|
const loops = getStore().list();
|
|
68
68
|
if (loops.length === 0) {
|
|
69
|
-
await ui.select("No
|
|
69
|
+
await ui.select("No loops configured", ["< Back"]);
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -81,7 +81,7 @@ export function registerLoopCommand(options: LoopCommandOptions): void {
|
|
|
81
81
|
});
|
|
82
82
|
choices.push("< Back");
|
|
83
83
|
|
|
84
|
-
const selected = await ui.select("
|
|
84
|
+
const selected = await ui.select("Loops", choices);
|
|
85
85
|
if (!selected || selected === "< Back") return;
|
|
86
86
|
|
|
87
87
|
const match = selected.match(/#(\d+)/);
|
|
@@ -136,14 +136,14 @@ export function registerLoopCommand(options: LoopCommandOptions): void {
|
|
|
136
136
|
const choice = await ui.select("Loop", [
|
|
137
137
|
"Create scheduled loop",
|
|
138
138
|
"Create event-triggered loop",
|
|
139
|
-
"View
|
|
139
|
+
"View loops",
|
|
140
140
|
"Settings",
|
|
141
141
|
]);
|
|
142
142
|
|
|
143
143
|
if (!choice) return;
|
|
144
144
|
if (choice.startsWith("Create scheduled")) return scheduleLoop(ui);
|
|
145
145
|
if (choice.startsWith("Create event")) return eventLoop(ui);
|
|
146
|
-
if (choice.startsWith("View
|
|
146
|
+
if (choice.startsWith("View loops")) return viewLoops(ui);
|
|
147
147
|
return settings(ui);
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -173,7 +173,6 @@ export function registerLoopCommand(options: LoopCommandOptions): void {
|
|
|
173
173
|
const choice = await ui.select("Loop mode", [
|
|
174
174
|
`Scheduled: "${trimmed.slice(0, 50)}"`,
|
|
175
175
|
`Event-triggered: "${trimmed.slice(0, 50)}"`,
|
|
176
|
-
`Self-paced: "${trimmed.slice(0, 50)}"`,
|
|
177
176
|
]);
|
|
178
177
|
|
|
179
178
|
if (!choice) return;
|
|
@@ -45,7 +45,7 @@ export function registerTasksCommand(options: TasksCommandOptions): void {
|
|
|
45
45
|
const backlog = await emitCreated(entry);
|
|
46
46
|
ui.notify(`Task #${entry.id} created`, "info");
|
|
47
47
|
if (backlog.created && backlog.entry) {
|
|
48
|
-
ui.notify(`
|
|
48
|
+
ui.notify(`Backlog worker loop #${backlog.entry.id} created`, "info");
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
@@ -64,7 +64,7 @@ export function registerTasksCommand(options: TasksCommandOptions): void {
|
|
|
64
64
|
choices.unshift("+ Create task");
|
|
65
65
|
choices.push("< Back");
|
|
66
66
|
|
|
67
|
-
const selected = await ui.select("
|
|
67
|
+
const selected = await ui.select("Tasks", choices);
|
|
68
68
|
if (!selected || selected === "< Back") return;
|
|
69
69
|
if (selected === "+ Create task") {
|
|
70
70
|
await createNativeTaskInteractively(ui);
|
|
@@ -125,7 +125,7 @@ export function registerTasksCommand(options: TasksCommandOptions): void {
|
|
|
125
125
|
const backlog = await emitCreated(entry);
|
|
126
126
|
ctx.ui.notify(`Task #${entry.id} created`, "info");
|
|
127
127
|
if (backlog.created && backlog.entry) {
|
|
128
|
-
ctx.ui.notify(`
|
|
128
|
+
ctx.ui.notify(`Backlog worker loop #${backlog.entry.id} created`, "info");
|
|
129
129
|
}
|
|
130
130
|
return;
|
|
131
131
|
}
|
package/src/goal-reducer.ts
CHANGED
|
@@ -82,6 +82,15 @@ export type GoalReducerEffect =
|
|
|
82
82
|
entityType: "goal";
|
|
83
83
|
entityId: string;
|
|
84
84
|
payload: { id: string };
|
|
85
|
+
}
|
|
86
|
+
| {
|
|
87
|
+
// Diagnostic: a transition was dropped because the goal is in a terminal
|
|
88
|
+
// state. Surfaced (rather than silently ignored) so the effect sink can log
|
|
89
|
+
// it. Carries no state change.
|
|
90
|
+
type: "GOAL_TRANSITION_REJECTED";
|
|
91
|
+
entityType: "goal";
|
|
92
|
+
entityId: string;
|
|
93
|
+
payload: { id: string; status: GoalEntry["status"]; attempted: string };
|
|
85
94
|
};
|
|
86
95
|
|
|
87
96
|
export interface GoalReduceResult {
|
|
@@ -160,7 +169,17 @@ export function reduceGoalState(state: GoalReducerState, event: GoalReducerEvent
|
|
|
160
169
|
const id = event.payload.id;
|
|
161
170
|
const current = state.goalsById[id];
|
|
162
171
|
if (!current) return { state, effects: [] };
|
|
163
|
-
if (isTerminal(current) && event.type !== "GOAL_ARCHIVED")
|
|
172
|
+
if (isTerminal(current) && event.type !== "GOAL_ARCHIVED") {
|
|
173
|
+
return {
|
|
174
|
+
state,
|
|
175
|
+
effects: [{
|
|
176
|
+
type: "GOAL_TRANSITION_REJECTED",
|
|
177
|
+
entityType: "goal",
|
|
178
|
+
entityId: id,
|
|
179
|
+
payload: { id, status: current.status, attempted: event.type },
|
|
180
|
+
}],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
164
183
|
|
|
165
184
|
const next = cloneState(state);
|
|
166
185
|
const goal: GoalEntry = {
|
package/src/goal-store.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
1
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
2
|
+
import { join } from "node:path";
|
|
4
3
|
import { type GoalReducerEvent, reduceGoalState } from "./goal-reducer.js";
|
|
5
4
|
import type {
|
|
6
5
|
GoalCriteria,
|
|
@@ -10,120 +9,24 @@ import type {
|
|
|
10
9
|
GoalScope,
|
|
11
10
|
GoalStoreData,
|
|
12
11
|
} from "./goal-types.js";
|
|
12
|
+
import { ReducerBackedStore } from "./reducer-backed-store.js";
|
|
13
13
|
|
|
14
14
|
const GOALS_DIR = join(homedir(), ".pi", "goals");
|
|
15
|
-
const LOCK_RETRY_MS = 50;
|
|
16
|
-
const LOCK_MAX_RETRIES = 100;
|
|
17
15
|
const MAX_GOALS = 200;
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
|
|
21
|
-
try {
|
|
22
|
-
writeFileSync(lockPath, `${process.pid}`, { flag: "wx" });
|
|
23
|
-
return;
|
|
24
|
-
} catch (e: any) {
|
|
25
|
-
if (e.code === "EEXIST") {
|
|
26
|
-
try {
|
|
27
|
-
const pid = parseInt(readFileSync(lockPath, "utf-8"), 10);
|
|
28
|
-
if (!pid || !isProcessRunning(pid)) {
|
|
29
|
-
try { unlinkSync(lockPath); } catch { /* ignore */ }
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
} catch { /* ignore read errors */ }
|
|
33
|
-
const start = Date.now();
|
|
34
|
-
while (Date.now() - start < LOCK_RETRY_MS) { /* busy wait */ }
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
throw e;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
throw new Error(`Failed to acquire lock: ${lockPath}`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function releaseLock(lockPath: string): void {
|
|
44
|
-
try { unlinkSync(lockPath); } catch { /* ignore */ }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function isProcessRunning(pid: number): boolean {
|
|
48
|
-
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export class GoalStore {
|
|
52
|
-
private filePath: string | undefined;
|
|
53
|
-
private lockPath: string | undefined;
|
|
54
|
-
private lastLoadedSignature: string | undefined;
|
|
55
|
-
|
|
56
|
-
private nextId = 1;
|
|
57
|
-
private goals = new Map<string, GoalEntry>();
|
|
58
|
-
|
|
17
|
+
export class GoalStore extends ReducerBackedStore<GoalEntry, GoalReducerState, GoalReducerEvent, GoalStoreData> {
|
|
59
18
|
constructor(listIdOrPath?: string) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const stat = statSync(this.filePath);
|
|
72
|
-
return `${stat.mtimeMs}:${stat.size}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
private load(force = false): void {
|
|
76
|
-
if (!this.filePath) return;
|
|
77
|
-
const signature = this.getFileSignature();
|
|
78
|
-
if (!signature) return;
|
|
79
|
-
if (!force && signature === this.lastLoadedSignature) return;
|
|
80
|
-
try {
|
|
81
|
-
const data: GoalStoreData = JSON.parse(readFileSync(this.filePath, "utf-8"));
|
|
82
|
-
this.nextId = data.nextId;
|
|
83
|
-
this.goals.clear();
|
|
84
|
-
for (const goal of data.goals) {
|
|
85
|
-
this.goals.set(goal.id, goal);
|
|
86
|
-
}
|
|
87
|
-
this.lastLoadedSignature = signature;
|
|
88
|
-
} catch { /* corrupt file — start fresh */ }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private save(): void {
|
|
92
|
-
if (!this.filePath) return;
|
|
93
|
-
const data: GoalStoreData = {
|
|
94
|
-
nextId: this.nextId,
|
|
95
|
-
goals: Array.from(this.goals.values()),
|
|
96
|
-
};
|
|
97
|
-
const tmpPath = this.filePath + ".tmp";
|
|
98
|
-
writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
99
|
-
renameSync(tmpPath, this.filePath);
|
|
100
|
-
this.lastLoadedSignature = this.getFileSignature();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
private withLock<T>(fn: () => T): T {
|
|
104
|
-
if (!this.lockPath) return fn();
|
|
105
|
-
acquireLock(this.lockPath);
|
|
106
|
-
try {
|
|
107
|
-
this.load(true);
|
|
108
|
-
const result = fn();
|
|
109
|
-
this.save();
|
|
110
|
-
return result;
|
|
111
|
-
} finally {
|
|
112
|
-
releaseLock(this.lockPath);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
private toReducerState(): GoalReducerState {
|
|
117
|
-
return {
|
|
118
|
-
nextId: this.nextId,
|
|
119
|
-
goalsById: Object.fromEntries(this.goals.entries()),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
private applyReducerEvent(event: GoalReducerEvent): void {
|
|
124
|
-
const result = reduceGoalState(this.toReducerState(), event);
|
|
125
|
-
this.nextId = result.state.nextId;
|
|
126
|
-
this.goals = new Map(Object.entries(result.state.goalsById));
|
|
19
|
+
super(
|
|
20
|
+
{
|
|
21
|
+
baseDir: GOALS_DIR,
|
|
22
|
+
reduce: (state, event) => reduceGoalState(state, event),
|
|
23
|
+
toReducerState: (nextId, entries) => ({ nextId, goalsById: Object.fromEntries(entries.entries()) }),
|
|
24
|
+
fromReducerState: (state) => ({ nextId: state.nextId, entries: new Map(Object.entries(state.goalsById)) }),
|
|
25
|
+
serialize: (nextId, entries) => ({ nextId, goals: Array.from(entries.values()) }),
|
|
26
|
+
deserialize: (data) => ({ nextId: data.nextId, entries: new Map(data.goals.map((g) => [g.id, g])) }),
|
|
27
|
+
},
|
|
28
|
+
listIdOrPath,
|
|
29
|
+
);
|
|
127
30
|
}
|
|
128
31
|
|
|
129
32
|
create(
|
|
@@ -134,7 +37,7 @@ export class GoalStore {
|
|
|
134
37
|
metadata?: Record<string, unknown>,
|
|
135
38
|
): GoalEntry {
|
|
136
39
|
return this.withLock(() => {
|
|
137
|
-
if (this.
|
|
40
|
+
if (this.entries.size >= MAX_GOALS) {
|
|
138
41
|
throw new Error(`Maximum of ${MAX_GOALS} goals reached. Archive some before creating new ones.`);
|
|
139
42
|
}
|
|
140
43
|
this.applyReducerEvent({
|
|
@@ -150,23 +53,13 @@ export class GoalStore {
|
|
|
150
53
|
metadata,
|
|
151
54
|
},
|
|
152
55
|
});
|
|
153
|
-
return this.
|
|
56
|
+
return this.entries.get(String(this.nextId - 1))!;
|
|
154
57
|
});
|
|
155
58
|
}
|
|
156
59
|
|
|
157
|
-
get(id: string): GoalEntry | undefined {
|
|
158
|
-
if (this.filePath) this.load();
|
|
159
|
-
return this.goals.get(id);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
list(): GoalEntry[] {
|
|
163
|
-
if (this.filePath) this.load();
|
|
164
|
-
return Array.from(this.goals.values()).sort((a, b) => Number(a.id) - Number(b.id));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
60
|
activate(id: string): GoalEntry | undefined {
|
|
168
61
|
return this.withLock(() => {
|
|
169
|
-
if (!this.
|
|
62
|
+
if (!this.entries.has(id)) return undefined;
|
|
170
63
|
this.applyReducerEvent({
|
|
171
64
|
type: "GOAL_ACTIVATED",
|
|
172
65
|
at: Date.now(),
|
|
@@ -175,13 +68,13 @@ export class GoalStore {
|
|
|
175
68
|
entityId: id,
|
|
176
69
|
payload: { id },
|
|
177
70
|
});
|
|
178
|
-
return this.
|
|
71
|
+
return this.entries.get(id);
|
|
179
72
|
});
|
|
180
73
|
}
|
|
181
74
|
|
|
182
75
|
recordProgress(id: string, progress: GoalProgressSnapshot): GoalEntry | undefined {
|
|
183
76
|
return this.withLock(() => {
|
|
184
|
-
if (!this.
|
|
77
|
+
if (!this.entries.has(id)) return undefined;
|
|
185
78
|
this.applyReducerEvent({
|
|
186
79
|
type: "GOAL_PROGRESS_RECORDED",
|
|
187
80
|
at: Date.now(),
|
|
@@ -190,13 +83,13 @@ export class GoalStore {
|
|
|
190
83
|
entityId: id,
|
|
191
84
|
payload: { id, progress },
|
|
192
85
|
});
|
|
193
|
-
return this.
|
|
86
|
+
return this.entries.get(id);
|
|
194
87
|
});
|
|
195
88
|
}
|
|
196
89
|
|
|
197
90
|
markVerificationStarted(id: string): GoalEntry | undefined {
|
|
198
91
|
return this.withLock(() => {
|
|
199
|
-
if (!this.
|
|
92
|
+
if (!this.entries.has(id)) return undefined;
|
|
200
93
|
this.applyReducerEvent({
|
|
201
94
|
type: "GOAL_VERIFICATION_STARTED",
|
|
202
95
|
at: Date.now(),
|
|
@@ -205,13 +98,13 @@ export class GoalStore {
|
|
|
205
98
|
entityId: id,
|
|
206
99
|
payload: { id },
|
|
207
100
|
});
|
|
208
|
-
return this.
|
|
101
|
+
return this.entries.get(id);
|
|
209
102
|
});
|
|
210
103
|
}
|
|
211
104
|
|
|
212
105
|
markVerified(id: string, reason: string, progress?: GoalProgressSnapshot): GoalEntry | undefined {
|
|
213
106
|
return this.withLock(() => {
|
|
214
|
-
if (!this.
|
|
107
|
+
if (!this.entries.has(id)) return undefined;
|
|
215
108
|
this.applyReducerEvent({
|
|
216
109
|
type: "GOAL_VERIFICATION_PASSED",
|
|
217
110
|
at: Date.now(),
|
|
@@ -220,13 +113,13 @@ export class GoalStore {
|
|
|
220
113
|
entityId: id,
|
|
221
114
|
payload: { id, reason, progress },
|
|
222
115
|
});
|
|
223
|
-
return this.
|
|
116
|
+
return this.entries.get(id);
|
|
224
117
|
});
|
|
225
118
|
}
|
|
226
119
|
|
|
227
120
|
markFailed(id: string, reason: string, progress?: GoalProgressSnapshot): GoalEntry | undefined {
|
|
228
121
|
return this.withLock(() => {
|
|
229
|
-
if (!this.
|
|
122
|
+
if (!this.entries.has(id)) return undefined;
|
|
230
123
|
this.applyReducerEvent({
|
|
231
124
|
type: "GOAL_FAILED",
|
|
232
125
|
at: Date.now(),
|
|
@@ -235,13 +128,13 @@ export class GoalStore {
|
|
|
235
128
|
entityId: id,
|
|
236
129
|
payload: { id, reason, progress },
|
|
237
130
|
});
|
|
238
|
-
return this.
|
|
131
|
+
return this.entries.get(id);
|
|
239
132
|
});
|
|
240
133
|
}
|
|
241
134
|
|
|
242
135
|
markBlocked(id: string, reason: string, progress?: GoalProgressSnapshot): GoalEntry | undefined {
|
|
243
136
|
return this.withLock(() => {
|
|
244
|
-
if (!this.
|
|
137
|
+
if (!this.entries.has(id)) return undefined;
|
|
245
138
|
this.applyReducerEvent({
|
|
246
139
|
type: "GOAL_BLOCKED",
|
|
247
140
|
at: Date.now(),
|
|
@@ -250,13 +143,13 @@ export class GoalStore {
|
|
|
250
143
|
entityId: id,
|
|
251
144
|
payload: { id, reason, progress },
|
|
252
145
|
});
|
|
253
|
-
return this.
|
|
146
|
+
return this.entries.get(id);
|
|
254
147
|
});
|
|
255
148
|
}
|
|
256
149
|
|
|
257
150
|
unblock(id: string): GoalEntry | undefined {
|
|
258
151
|
return this.withLock(() => {
|
|
259
|
-
if (!this.
|
|
152
|
+
if (!this.entries.has(id)) return undefined;
|
|
260
153
|
this.applyReducerEvent({
|
|
261
154
|
type: "GOAL_UNBLOCKED",
|
|
262
155
|
at: Date.now(),
|
|
@@ -265,7 +158,7 @@ export class GoalStore {
|
|
|
265
158
|
entityId: id,
|
|
266
159
|
payload: { id },
|
|
267
160
|
});
|
|
268
|
-
return this.
|
|
161
|
+
return this.entries.get(id);
|
|
269
162
|
});
|
|
270
163
|
}
|
|
271
164
|
|
|
@@ -280,7 +173,7 @@ export class GoalStore {
|
|
|
280
173
|
},
|
|
281
174
|
): GoalEntry | undefined {
|
|
282
175
|
return this.withLock(() => {
|
|
283
|
-
if (!this.
|
|
176
|
+
if (!this.entries.has(id)) return undefined;
|
|
284
177
|
if (
|
|
285
178
|
fields.title === undefined
|
|
286
179
|
&& fields.description === undefined
|
|
@@ -288,7 +181,7 @@ export class GoalStore {
|
|
|
288
181
|
&& fields.criteria === undefined
|
|
289
182
|
&& fields.metadata === undefined
|
|
290
183
|
) {
|
|
291
|
-
return this.
|
|
184
|
+
return this.entries.get(id);
|
|
292
185
|
}
|
|
293
186
|
this.applyReducerEvent({
|
|
294
187
|
type: "GOAL_UPDATED",
|
|
@@ -305,13 +198,13 @@ export class GoalStore {
|
|
|
305
198
|
metadata: fields.metadata,
|
|
306
199
|
},
|
|
307
200
|
});
|
|
308
|
-
return this.
|
|
201
|
+
return this.entries.get(id);
|
|
309
202
|
});
|
|
310
203
|
}
|
|
311
204
|
|
|
312
205
|
archive(id: string, reason?: string): GoalEntry | undefined {
|
|
313
206
|
return this.withLock(() => {
|
|
314
|
-
if (!this.
|
|
207
|
+
if (!this.entries.has(id)) return undefined;
|
|
315
208
|
this.applyReducerEvent({
|
|
316
209
|
type: "GOAL_ARCHIVED",
|
|
317
210
|
at: Date.now(),
|
|
@@ -320,7 +213,7 @@ export class GoalStore {
|
|
|
320
213
|
entityId: id,
|
|
321
214
|
payload: { id, reason },
|
|
322
215
|
});
|
|
323
|
-
return this.
|
|
216
|
+
return this.entries.get(id);
|
|
324
217
|
});
|
|
325
218
|
}
|
|
326
219
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,13 +10,14 @@
|
|
|
10
10
|
* MonitorStop — Stop a running monitor
|
|
11
11
|
*
|
|
12
12
|
* Commands:
|
|
13
|
-
* /loop — Schedule
|
|
14
|
-
* /
|
|
13
|
+
* /loop — Schedule or manage re-wake loops: /loop [interval] [prompt]
|
|
14
|
+
* /tasks — View or manage native fallback tasks when pi-tasks is absent
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import { registerLoopCommand } from "./commands/loop-command.js";
|
|
19
19
|
import { registerTasksCommand } from "./commands/tasks-command.js";
|
|
20
|
+
import { atMaxFires } from "./loop-reducer.js";
|
|
20
21
|
import { MonitorManager } from "./monitor-manager.js";
|
|
21
22
|
import { createMonitorOnDoneRuntime } from "./runtime/monitor-ondone-runtime.js";
|
|
22
23
|
import {
|
|
@@ -178,7 +179,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
178
179
|
function onLoopFire(entry: LoopEntry): void {
|
|
179
180
|
debug(`loop:fire #${entry.id}`, { prompt: entry.prompt.slice(0, 50) });
|
|
180
181
|
|
|
181
|
-
if (
|
|
182
|
+
if (atMaxFires(entry)) {
|
|
182
183
|
debug(`loop #${entry.id} — reached maxFires ${entry.maxFires}, expiring`);
|
|
183
184
|
store.delete(entry.id);
|
|
184
185
|
return;
|
package/src/loop-reducer.ts
CHANGED
|
@@ -2,6 +2,16 @@ import type { LoopEntry, Trigger } from "./types.js";
|
|
|
2
2
|
|
|
3
3
|
export const MAX_LOOP_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Whether a loop has reached its fire cap. Single source of truth for the
|
|
7
|
+
* `maxFires` check shared by the fire callbacks (`onLoopFire` pre-fire guard and
|
|
8
|
+
* `TriggerSystem.fireLoop` post-fire cleanup). Each caller keeps its own timing;
|
|
9
|
+
* only the predicate is shared.
|
|
10
|
+
*/
|
|
11
|
+
export function atMaxFires(loop: Pick<LoopEntry, "maxFires" | "fireCount">): boolean {
|
|
12
|
+
return !!loop.maxFires && (loop.fireCount ?? 0) >= loop.maxFires;
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
6
16
|
|
|
7
17
|
export interface LoopReducerState {
|
package/src/monitor-manager.ts
CHANGED
|
@@ -128,9 +128,7 @@ export class MonitorManager {
|
|
|
128
128
|
exitCode: code,
|
|
129
129
|
outputLines: current.outputLines,
|
|
130
130
|
});
|
|
131
|
-
|
|
132
|
-
for (const callback of bp.completionCallbacks) callback();
|
|
133
|
-
}
|
|
131
|
+
for (const callback of bp.completionCallbacks) callback();
|
|
134
132
|
bp.completionCallbacks = [];
|
|
135
133
|
for (const resolve of bp.waiters) resolve();
|
|
136
134
|
bp.waiters = [];
|
|
@@ -171,6 +169,7 @@ export class MonitorManager {
|
|
|
171
169
|
monitorId: id,
|
|
172
170
|
error: err.message,
|
|
173
171
|
});
|
|
172
|
+
for (const callback of bp.completionCallbacks) callback();
|
|
174
173
|
bp.completionCallbacks = [];
|
|
175
174
|
for (const resolve of bp.waiters) resolve();
|
|
176
175
|
bp.waiters = [];
|