@trevonistrevon/pi-loop 0.5.9 → 0.6.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/CHANGELOG.md +72 -0
- package/README.md +64 -5
- package/dist/api.d.ts +11 -0
- package/dist/api.js +10 -0
- package/dist/commands/loop-command.d.ts +9 -2
- package/dist/commands/loop-command.js +81 -51
- package/dist/commands/tasks-command.js +3 -3
- package/dist/coordinator.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +89 -19
- package/dist/loop-format.d.ts +3 -0
- package/dist/loop-format.js +20 -0
- package/dist/loop-parse.d.ts +1 -0
- package/dist/loop-parse.js +59 -6
- package/dist/loop-reducer.d.ts +13 -1
- package/dist/loop-reducer.js +26 -0
- package/dist/monitor-manager.js +1 -1
- package/dist/notification-reducer.d.ts +2 -0
- package/dist/reducer-backed-store.d.ts +1 -1
- package/dist/reducer-backed-store.js +1 -1
- package/dist/rpc/channels.d.ts +73 -0
- package/dist/rpc/channels.js +30 -0
- package/dist/rpc/cross-extension-rpc.d.ts +59 -0
- package/dist/rpc/cross-extension-rpc.js +117 -0
- package/dist/runtime/loop-events.d.ts +26 -0
- package/dist/runtime/loop-events.js +26 -0
- package/dist/runtime/monitor-ondone-runtime.js +1 -1
- package/dist/runtime/native-task-rpc.d.ts +22 -0
- package/dist/runtime/native-task-rpc.js +50 -0
- package/dist/runtime/notification-runtime.d.ts +2 -1
- package/dist/runtime/notification-runtime.js +19 -7
- package/dist/runtime/task-backlog-runtime.d.ts +5 -1
- package/dist/runtime/task-backlog-runtime.js +14 -7
- package/dist/runtime/task-mutations.d.ts +39 -0
- package/dist/runtime/task-mutations.js +68 -0
- package/dist/runtime/task-rpc.d.ts +4 -0
- package/dist/runtime/task-rpc.js +40 -58
- package/dist/scheduler.js +25 -7
- package/dist/store.d.ts +9 -1
- package/dist/store.js +57 -0
- package/dist/tools/loop-tools.d.ts +8 -0
- package/dist/tools/loop-tools.js +105 -15
- package/dist/tools/monitor-tools.js +1 -3
- package/dist/tools/native-task-tools.d.ts +2 -6
- package/dist/tools/native-task-tools.js +25 -55
- package/dist/tools/tool-result.d.ts +8 -0
- package/dist/tools/tool-result.js +4 -0
- package/dist/trigger-system.js +1 -1
- package/dist/types.d.ts +24 -1
- package/package.json +15 -2
- package/src/api.ts +36 -0
- package/src/commands/loop-command.ts +100 -55
- package/src/commands/tasks-command.ts +3 -3
- package/src/coordinator.ts +1 -1
- package/src/index.ts +92 -19
- package/src/loop-format.ts +22 -0
- package/src/loop-parse.ts +62 -8
- package/src/loop-reducer.ts +41 -1
- package/src/monitor-manager.ts +1 -1
- package/src/notification-reducer.ts +3 -0
- package/src/reducer-backed-store.ts +3 -3
- package/src/rpc/channels.ts +97 -0
- package/src/rpc/cross-extension-rpc.ts +152 -0
- package/src/runtime/loop-events.ts +60 -0
- package/src/runtime/monitor-ondone-runtime.ts +1 -1
- package/src/runtime/native-task-rpc.ts +139 -0
- package/src/runtime/notification-runtime.ts +21 -8
- package/src/runtime/task-backlog-runtime.ts +25 -7
- package/src/runtime/task-mutations.ts +103 -0
- package/src/runtime/task-rpc.ts +48 -54
- package/src/scheduler.ts +25 -8
- package/src/store.ts +59 -2
- package/src/tools/loop-tools.ts +129 -16
- package/src/tools/monitor-tools.ts +1 -4
- package/src/tools/native-task-tools.ts +31 -52
- package/src/tools/tool-result.ts +4 -0
- package/src/trigger-system.ts +5 -5
- package/src/types.ts +29 -1
- package/.release-please-manifest.json +0 -3
- package/AGENTS.md +0 -83
- package/CONTRIBUTING.md +0 -69
- package/Dockerfile +0 -17
- package/benchmarks/experiment-sim.js +0 -30
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/coverage-final.json +0 -33
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -176
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/commands/index.html +0 -131
- package/coverage/src/commands/loop-command.ts.html +0 -634
- package/coverage/src/commands/tasks-command.ts.html +0 -490
- package/coverage/src/coordinator.ts.html +0 -430
- package/coverage/src/goal-coordinator.ts.html +0 -259
- package/coverage/src/goal-reducer.ts.html +0 -982
- package/coverage/src/goal-store.ts.html +0 -742
- package/coverage/src/goal-verifier.ts.html +0 -808
- package/coverage/src/index.html +0 -386
- package/coverage/src/index.ts.html +0 -1111
- package/coverage/src/loop-parse.ts.html +0 -574
- package/coverage/src/loop-reducer.ts.html +0 -559
- package/coverage/src/monitor-completion-coordinator.ts.html +0 -157
- package/coverage/src/monitor-manager.ts.html +0 -940
- package/coverage/src/monitor-reducer.ts.html +0 -583
- package/coverage/src/notification-reducer.ts.html +0 -550
- package/coverage/src/reducer-backed-store.ts.html +0 -589
- package/coverage/src/runtime/index.html +0 -206
- package/coverage/src/runtime/monitor-ondone-runtime.ts.html +0 -310
- package/coverage/src/runtime/notification-runtime.ts.html +0 -721
- package/coverage/src/runtime/scope.ts.html +0 -196
- package/coverage/src/runtime/session-runtime.ts.html +0 -679
- package/coverage/src/runtime/task-backlog-runtime.ts.html +0 -574
- package/coverage/src/runtime/task-events.ts.html +0 -208
- package/coverage/src/runtime/task-rpc.ts.html +0 -541
- package/coverage/src/scheduler.ts.html +0 -400
- package/coverage/src/store.ts.html +0 -667
- package/coverage/src/task-backlog-coordinator.ts.html +0 -181
- package/coverage/src/task-reducer.ts.html +0 -550
- package/coverage/src/task-store.ts.html +0 -526
- package/coverage/src/tools/index.html +0 -146
- package/coverage/src/tools/loop-tools.ts.html +0 -1000
- package/coverage/src/tools/monitor-tools.ts.html +0 -547
- package/coverage/src/tools/native-task-tools.ts.html +0 -556
- package/coverage/src/trigger-system.ts.html +0 -547
- package/coverage/src/ui/index.html +0 -116
- package/coverage/src/ui/widget.ts.html +0 -292
- package/dist/goal-coordinator.d.ts +0 -22
- package/dist/goal-coordinator.js +0 -28
- package/dist/goal-reducer.d.ts +0 -98
- package/dist/goal-reducer.js +0 -190
- package/dist/goal-store.d.ts +0 -22
- package/dist/goal-store.js +0 -188
- package/dist/goal-types.d.ts +0 -82
- package/dist/goal-types.js +0 -1
- package/dist/goal-verifier.d.ts +0 -20
- package/dist/goal-verifier.js +0 -198
- package/docker-compose.yml +0 -12
- package/docs/architecture/goal-state-schema.md +0 -505
- package/docs/architecture/state-machine-migration.md +0 -546
- package/docs/architecture/state-machine-reducer-event-model.md +0 -823
- package/docs/architecture/state-machine-test-matrix.md +0 -249
- package/docs/architecture/state-machine-transition-map.md +0 -436
- package/release-please-config.json +0 -9
- package/src/goal-coordinator.ts +0 -58
- package/src/goal-reducer.ts +0 -299
- package/src/goal-store.ts +0 -219
- package/src/goal-types.ts +0 -104
- package/src/goal-verifier.ts +0 -241
- package/vitest.config.ts +0 -29
package/dist/index.js
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* Tools:
|
|
5
5
|
* LoopCreate — Create a scheduled or event-triggered re-wake loop
|
|
6
6
|
* LoopList — List all active loops with status and next-fire times
|
|
7
|
+
* LoopUpdate — Continue, pause, or complete a dynamic goal loop
|
|
7
8
|
* LoopDelete — Delete or pause a loop by ID
|
|
8
9
|
* MonitorCreate — Start a background command that streams output via pi events
|
|
9
10
|
* MonitorList — List running monitors
|
|
10
11
|
* MonitorStop — Stop a running monitor
|
|
11
12
|
*
|
|
12
13
|
* Commands:
|
|
13
|
-
* /loop — Schedule or manage re-wake loops
|
|
14
|
+
* /loop — Schedule or manage re-wake loops, including /loop <goal>
|
|
14
15
|
* /tasks — View or manage native fallback tasks when pi-tasks is absent
|
|
15
16
|
*/
|
|
16
17
|
import { registerLoopCommand } from "./commands/loop-command.js";
|
|
@@ -18,6 +19,7 @@ import { registerTasksCommand } from "./commands/tasks-command.js";
|
|
|
18
19
|
import { atMaxFires } from "./loop-reducer.js";
|
|
19
20
|
import { MonitorManager } from "./monitor-manager.js";
|
|
20
21
|
import { createMonitorOnDoneRuntime } from "./runtime/monitor-ondone-runtime.js";
|
|
22
|
+
import { registerNativeTaskRpc } from "./runtime/native-task-rpc.js";
|
|
21
23
|
import { createNotificationRuntime, } from "./runtime/notification-runtime.js";
|
|
22
24
|
import { resolveLoopStorePath, resolveTaskStorePath } from "./runtime/scope.js";
|
|
23
25
|
import { registerSessionRuntimeHooks } from "./runtime/session-runtime.js";
|
|
@@ -53,24 +55,46 @@ export default function (pi) {
|
|
|
53
55
|
// call), so stale monitors don't linger in the count between turns.
|
|
54
56
|
monitorManager.setOnChange(() => widget.update());
|
|
55
57
|
widget.setTaskSummaryProvider(() => {
|
|
56
|
-
|
|
58
|
+
// Once pi-tasks owns the channels, any native store created during the
|
|
59
|
+
// detection window is a shadow — never surface it in the status line.
|
|
60
|
+
if (tasksAvailable || !nativeTaskStore)
|
|
57
61
|
return { count: 0 };
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
let count = 0;
|
|
63
|
+
let activeSubject;
|
|
64
|
+
let nextSubject;
|
|
65
|
+
for (const task of nativeTaskStore.list()) {
|
|
66
|
+
if (task.status === "in_progress") {
|
|
67
|
+
count++;
|
|
68
|
+
activeSubject ??= task.subject;
|
|
69
|
+
}
|
|
70
|
+
else if (task.status === "pending") {
|
|
71
|
+
count++;
|
|
72
|
+
nextSubject ??= task.subject;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const focus = activeSubject
|
|
76
|
+
? `active: ${activeSubject.slice(0, 50)}`
|
|
77
|
+
: nextSubject
|
|
78
|
+
? `next: ${nextSubject.slice(0, 50)}`
|
|
65
79
|
: undefined;
|
|
66
|
-
return { count
|
|
80
|
+
return { count, focusText: focus };
|
|
67
81
|
});
|
|
68
82
|
scheduler = new CronScheduler(store, onLoopFire);
|
|
69
83
|
triggerSystem = new TriggerSystem(pi, scheduler, store, onLoopFire);
|
|
70
84
|
// ── pi-tasks integration ──
|
|
71
85
|
let tasksAvailable = false;
|
|
86
|
+
let tasksDetectionSettled = false;
|
|
72
87
|
let nativeTaskStore;
|
|
73
88
|
let nativeTasksRegistered = false;
|
|
89
|
+
function getOrCreateNativeTaskStore() {
|
|
90
|
+
// pi-tasks owns the task channels; don't create a shadow store.
|
|
91
|
+
if (tasksAvailable)
|
|
92
|
+
return undefined;
|
|
93
|
+
if (!nativeTaskStore) {
|
|
94
|
+
nativeTaskStore = new TaskStore(resolveTaskStorePath(getScopeOptions(), _sessionId));
|
|
95
|
+
}
|
|
96
|
+
return nativeTaskStore;
|
|
97
|
+
}
|
|
74
98
|
const taskRuntime = createTaskRuntimeBridge({
|
|
75
99
|
pi,
|
|
76
100
|
isTasksAvailable: () => tasksAvailable,
|
|
@@ -86,6 +110,29 @@ export default function (pi) {
|
|
|
86
110
|
widget.update();
|
|
87
111
|
await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
88
112
|
},
|
|
113
|
+
onDetectionStarted: () => {
|
|
114
|
+
tasksDetectionSettled = false;
|
|
115
|
+
},
|
|
116
|
+
onDetectionSettled: () => {
|
|
117
|
+
tasksDetectionSettled = true;
|
|
118
|
+
},
|
|
119
|
+
debug,
|
|
120
|
+
});
|
|
121
|
+
// The RPC server registers at init (not behind the 6s tool-fallback timer) so
|
|
122
|
+
// early cross-extension calls never race the timer; it stands down via
|
|
123
|
+
// isEnabled once an external pi-tasks is detected. checkTasksVersion ignores
|
|
124
|
+
// this server's own ping reply via its provider field. Mutating verbs stay
|
|
125
|
+
// silent until the detection probe settles, so a co-resident pi-tasks can
|
|
126
|
+
// never race the native server into creating divergent task state.
|
|
127
|
+
registerNativeTaskRpc({
|
|
128
|
+
pi,
|
|
129
|
+
getNativeTaskStore: getOrCreateNativeTaskStore,
|
|
130
|
+
isEnabled: () => !tasksAvailable,
|
|
131
|
+
isDetectionSettled: () => tasksDetectionSettled,
|
|
132
|
+
evaluateTaskBacklog: (taskStore, pendingCount) => evaluateTaskBacklog(taskStore, pendingCount),
|
|
133
|
+
updateWidget: () => {
|
|
134
|
+
widget.update();
|
|
135
|
+
},
|
|
89
136
|
debug,
|
|
90
137
|
});
|
|
91
138
|
taskRuntime.checkTasksVersion();
|
|
@@ -144,6 +191,9 @@ export default function (pi) {
|
|
|
144
191
|
deleteLoop: (id) => {
|
|
145
192
|
store.delete(id);
|
|
146
193
|
},
|
|
194
|
+
recordDeletionTombstone: (id, tombstone) => {
|
|
195
|
+
store.recordDeletionTombstone(id, tombstone);
|
|
196
|
+
},
|
|
147
197
|
addTrigger: (entry) => {
|
|
148
198
|
triggerSystem.add(entry);
|
|
149
199
|
},
|
|
@@ -156,6 +206,12 @@ export default function (pi) {
|
|
|
156
206
|
hasPendingTasks: () => hasPendingTasks(),
|
|
157
207
|
bootstrapTaskLoop: (entry) => maybeBootstrapTaskLoop(entry),
|
|
158
208
|
triggerHasEventSource,
|
|
209
|
+
emitLoopAutodeleted: (payload) => {
|
|
210
|
+
pi.events.emit("loops:autodeleted", payload);
|
|
211
|
+
},
|
|
212
|
+
emitTaskBacklogEmpty: (payload) => {
|
|
213
|
+
pi.events.emit("tasks:backlog_empty", payload);
|
|
214
|
+
},
|
|
159
215
|
debug,
|
|
160
216
|
});
|
|
161
217
|
const flushPendingNotifications = notificationRuntime.flushPendingNotifications;
|
|
@@ -171,6 +227,16 @@ export default function (pi) {
|
|
|
171
227
|
return;
|
|
172
228
|
}
|
|
173
229
|
store.fire(entry.id);
|
|
230
|
+
const firedAt = Date.now();
|
|
231
|
+
const firedEntry = entry.trigger.type === "dynamic"
|
|
232
|
+
? store.updateDynamic(entry.id, {
|
|
233
|
+
dynamic: {
|
|
234
|
+
awaitingUpdate: true,
|
|
235
|
+
nextWakeAt: undefined,
|
|
236
|
+
lastUpdatedAt: firedAt,
|
|
237
|
+
},
|
|
238
|
+
}) ?? entry
|
|
239
|
+
: entry;
|
|
174
240
|
if (entry.autoTask) {
|
|
175
241
|
autoCreateTask(entry).then((taskId) => {
|
|
176
242
|
if (taskId)
|
|
@@ -178,13 +244,14 @@ export default function (pi) {
|
|
|
178
244
|
});
|
|
179
245
|
}
|
|
180
246
|
pi.events.emit("loop:fire", {
|
|
181
|
-
loopId:
|
|
182
|
-
prompt:
|
|
183
|
-
trigger:
|
|
184
|
-
timestamp:
|
|
185
|
-
readOnly:
|
|
186
|
-
recurring:
|
|
187
|
-
autoTask:
|
|
247
|
+
loopId: firedEntry.id,
|
|
248
|
+
prompt: firedEntry.prompt,
|
|
249
|
+
trigger: firedEntry.trigger,
|
|
250
|
+
timestamp: firedAt,
|
|
251
|
+
readOnly: firedEntry.readOnly,
|
|
252
|
+
recurring: firedEntry.recurring,
|
|
253
|
+
autoTask: firedEntry.autoTask,
|
|
254
|
+
dynamic: firedEntry.dynamic,
|
|
188
255
|
});
|
|
189
256
|
}
|
|
190
257
|
// ── Session lifecycle ──
|
|
@@ -266,10 +333,14 @@ export default function (pi) {
|
|
|
266
333
|
},
|
|
267
334
|
});
|
|
268
335
|
// ── Native task tools (only when pi-tasks is absent) ──
|
|
336
|
+
// Only tool/command registration stays deferred: the tool names collide with
|
|
337
|
+
// pi-tasks and cannot be unregistered. The RPC server is already live (above).
|
|
269
338
|
const nativeTaskFallbackTimer = setTimeout(() => {
|
|
270
339
|
if (tasksAvailable || nativeTasksRegistered)
|
|
271
340
|
return;
|
|
272
|
-
const taskStore =
|
|
341
|
+
const taskStore = getOrCreateNativeTaskStore();
|
|
342
|
+
if (!taskStore)
|
|
343
|
+
return;
|
|
273
344
|
try {
|
|
274
345
|
registerTasksCommand({
|
|
275
346
|
pi,
|
|
@@ -295,7 +366,6 @@ export default function (pi) {
|
|
|
295
366
|
}
|
|
296
367
|
throw error;
|
|
297
368
|
}
|
|
298
|
-
nativeTaskStore = taskStore;
|
|
299
369
|
nativeTasksRegistered = true;
|
|
300
370
|
debug("native task tools registered (pi-tasks not detected)");
|
|
301
371
|
}, 6000);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function formatTrigger(trigger, style = "list") {
|
|
2
|
+
if (typeof trigger === "string")
|
|
3
|
+
return trigger;
|
|
4
|
+
if (trigger.type === "cron") {
|
|
5
|
+
return style === "create" || style === "notification"
|
|
6
|
+
? `schedule: ${trigger.schedule}`
|
|
7
|
+
: `cron: ${trigger.schedule}`;
|
|
8
|
+
}
|
|
9
|
+
if (trigger.type === "event")
|
|
10
|
+
return `event: ${trigger.source}`;
|
|
11
|
+
if (trigger.type === "dynamic")
|
|
12
|
+
return "dynamic";
|
|
13
|
+
if (style === "command")
|
|
14
|
+
return `hybrid: ${trigger.cron}`;
|
|
15
|
+
if (style === "create")
|
|
16
|
+
return `hybrid: cron ${trigger.cron} + event ${trigger.event.source}`;
|
|
17
|
+
if (style === "notification")
|
|
18
|
+
return "hybrid";
|
|
19
|
+
return `hybrid: ${trigger.cron} + ${trigger.event.source}`;
|
|
20
|
+
}
|
package/dist/loop-parse.d.ts
CHANGED
package/dist/loop-parse.js
CHANGED
|
@@ -21,6 +21,8 @@ const COMMON_INTERVALS = {
|
|
|
21
21
|
86400: "0 0 * * *",
|
|
22
22
|
};
|
|
23
23
|
function roundToNearestCommon(seconds) {
|
|
24
|
+
// COMMON_INTERVALS is a non-empty const table, so keys[0] and
|
|
25
|
+
// COMMON_INTERVALS[best] below are true invariants, not runtime fallbacks.
|
|
24
26
|
const keys = Object.keys(COMMON_INTERVALS).map(Number).sort((a, b) => a - b);
|
|
25
27
|
let best = keys[0];
|
|
26
28
|
for (const k of keys) {
|
|
@@ -48,15 +50,59 @@ function isFullCron(expr) {
|
|
|
48
50
|
const parts = expr.trim().split(/\s+/);
|
|
49
51
|
return parts.length === 5;
|
|
50
52
|
}
|
|
53
|
+
function parseCronNumber(input, min, max) {
|
|
54
|
+
if (!/^\d+$/.test(input))
|
|
55
|
+
return undefined;
|
|
56
|
+
const value = Number.parseInt(input, 10);
|
|
57
|
+
return value >= min && value <= max ? value : undefined;
|
|
58
|
+
}
|
|
59
|
+
function isValidCronField(field, min, max) {
|
|
60
|
+
return field.split(",").every((part) => {
|
|
61
|
+
const [base, step, extra] = part.split("/");
|
|
62
|
+
if (extra !== undefined || base === undefined)
|
|
63
|
+
return false;
|
|
64
|
+
if (step !== undefined && (parseCronNumber(step, 1, max - min + 1) === undefined))
|
|
65
|
+
return false;
|
|
66
|
+
if (base === "*")
|
|
67
|
+
return true;
|
|
68
|
+
const range = base.split("-");
|
|
69
|
+
if (range.length === 1)
|
|
70
|
+
return step === undefined && parseCronNumber(base, min, max) !== undefined;
|
|
71
|
+
if (range.length !== 2)
|
|
72
|
+
return false;
|
|
73
|
+
const start = parseCronNumber(range[0] ?? "", min, max);
|
|
74
|
+
const end = parseCronNumber(range[1] ?? "", min, max);
|
|
75
|
+
return start !== undefined && end !== undefined && start <= end;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export function isValidCronExpression(expr) {
|
|
79
|
+
const fields = expr.trim().split(/\s+/);
|
|
80
|
+
if (fields.length !== 5)
|
|
81
|
+
return false;
|
|
82
|
+
const ranges = [
|
|
83
|
+
[0, 59],
|
|
84
|
+
[0, 23],
|
|
85
|
+
[1, 31],
|
|
86
|
+
[1, 12],
|
|
87
|
+
[0, 6],
|
|
88
|
+
];
|
|
89
|
+
return fields.every((field, index) => {
|
|
90
|
+
const range = ranges[index];
|
|
91
|
+
return range !== undefined && isValidCronField(field, range[0], range[1]);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
51
94
|
export function parseInterval(input) {
|
|
52
95
|
const trimmed = input.trim();
|
|
53
96
|
if (isFullCron(trimmed)) {
|
|
97
|
+
if (!isValidCronExpression(trimmed)) {
|
|
98
|
+
throw new Error(`Invalid cron expression: ${trimmed}`);
|
|
99
|
+
}
|
|
54
100
|
return { cron: trimmed, description: `cron: ${trimmed}` };
|
|
55
101
|
}
|
|
56
102
|
const match = trimmed.match(/^(\d+)\s*(s|m|h|d)$/i);
|
|
57
103
|
if (match) {
|
|
58
|
-
const value = parseInt(match[1], 10);
|
|
59
|
-
const unit = match[2].toLowerCase();
|
|
104
|
+
const value = parseInt(match[1] ?? "", 10);
|
|
105
|
+
const unit = (match[2] ?? "").toLowerCase();
|
|
60
106
|
const totalSec = value * (UNIT_TO_CRON[unit] ?? 60);
|
|
61
107
|
if (totalSec < 60) {
|
|
62
108
|
return { cron: `*/1 * * * *`, description: `${totalSec} seconds (rounded to 1 minute)` };
|
|
@@ -67,9 +113,16 @@ export function parseInterval(input) {
|
|
|
67
113
|
}
|
|
68
114
|
export function cronToNextFire(cronExpr, fromDate = new Date()) {
|
|
69
115
|
const parts = cronExpr.trim().split(/\s+/);
|
|
70
|
-
if (
|
|
116
|
+
if (!isValidCronExpression(cronExpr))
|
|
71
117
|
throw new Error(`Invalid cron expression: ${cronExpr}`);
|
|
72
118
|
const [minF, hourF, dayF, monthF, dowF] = parts;
|
|
119
|
+
if (minF === undefined ||
|
|
120
|
+
hourF === undefined ||
|
|
121
|
+
dayF === undefined ||
|
|
122
|
+
monthF === undefined ||
|
|
123
|
+
dowF === undefined) {
|
|
124
|
+
throw new Error(`Invalid cron expression: ${cronExpr}`);
|
|
125
|
+
}
|
|
73
126
|
const now = new Date(fromDate);
|
|
74
127
|
now.setSeconds(0, 0);
|
|
75
128
|
for (let minutesAdvanced = 1; minutesAdvanced < 525600; minutesAdvanced++) {
|
|
@@ -96,7 +149,7 @@ function cronFieldMatches(field, value) {
|
|
|
96
149
|
if (part === "*")
|
|
97
150
|
return true;
|
|
98
151
|
if (part.includes("/")) {
|
|
99
|
-
const [range, stepStr] = part.split("/");
|
|
152
|
+
const [range = "", stepStr = ""] = part.split("/");
|
|
100
153
|
const step = parseInt(stepStr, 10);
|
|
101
154
|
let rangeMin;
|
|
102
155
|
let rangeMax;
|
|
@@ -105,7 +158,7 @@ function cronFieldMatches(field, value) {
|
|
|
105
158
|
rangeMax = 59;
|
|
106
159
|
}
|
|
107
160
|
else if (range.includes("-")) {
|
|
108
|
-
const [minS, maxS] = range.split("-");
|
|
161
|
+
const [minS = "", maxS = ""] = range.split("-");
|
|
109
162
|
rangeMin = parseInt(minS, 10);
|
|
110
163
|
rangeMax = parseInt(maxS, 10);
|
|
111
164
|
}
|
|
@@ -121,7 +174,7 @@ function cronFieldMatches(field, value) {
|
|
|
121
174
|
continue;
|
|
122
175
|
}
|
|
123
176
|
if (part.includes("-")) {
|
|
124
|
-
const [minS, maxS] = part.split("-");
|
|
177
|
+
const [minS = "", maxS = ""] = part.split("-");
|
|
125
178
|
const min = parseInt(minS, 10);
|
|
126
179
|
const max = parseInt(maxS, 10);
|
|
127
180
|
if (value >= min && value <= max)
|
package/dist/loop-reducer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LoopEntry, Trigger } from "./types.js";
|
|
1
|
+
import type { DynamicLoopState, LoopEntry, Trigger } from "./types.js";
|
|
2
2
|
export declare const MAX_LOOP_EXPIRY_MS: number;
|
|
3
3
|
/**
|
|
4
4
|
* Whether a loop has reached its fire cap. Single source of truth for the
|
|
@@ -26,6 +26,7 @@ export type LoopReducerEvent = {
|
|
|
26
26
|
taskBacklog?: boolean;
|
|
27
27
|
readOnly?: boolean;
|
|
28
28
|
maxFires?: number;
|
|
29
|
+
dynamic?: Partial<DynamicLoopState>;
|
|
29
30
|
};
|
|
30
31
|
} | {
|
|
31
32
|
type: "LOOP_PAUSED" | "LOOP_RESUMED" | "LOOP_FIRED" | "LOOP_DELETED" | "LOOP_MAX_FIRES_REACHED" | "LOOP_BACKLOG_EMPTY";
|
|
@@ -46,6 +47,17 @@ export type LoopReducerEvent = {
|
|
|
46
47
|
id: string;
|
|
47
48
|
reason: "expires_at" | "resume_event_stale" | "already_completed_monitor";
|
|
48
49
|
};
|
|
50
|
+
} | {
|
|
51
|
+
type: "LOOP_DYNAMIC_UPDATED";
|
|
52
|
+
at: number;
|
|
53
|
+
source: ReducerSource;
|
|
54
|
+
entityType?: "loop";
|
|
55
|
+
entityId?: string;
|
|
56
|
+
payload: {
|
|
57
|
+
id: string;
|
|
58
|
+
prompt?: string;
|
|
59
|
+
dynamic: Partial<DynamicLoopState>;
|
|
60
|
+
};
|
|
49
61
|
};
|
|
50
62
|
export type LoopReducerEffect = {
|
|
51
63
|
type: "PERSIST_LOOP";
|
package/dist/loop-reducer.js
CHANGED
|
@@ -32,6 +32,18 @@ export function reduceLoopState(state, event) {
|
|
|
32
32
|
readOnly: event.payload.readOnly,
|
|
33
33
|
maxFires: event.payload.maxFires,
|
|
34
34
|
fireCount: 0,
|
|
35
|
+
dynamic: event.payload.trigger.type === "dynamic" || event.payload.dynamic
|
|
36
|
+
? {
|
|
37
|
+
goal: event.payload.dynamic?.goal ?? event.payload.prompt,
|
|
38
|
+
state: event.payload.dynamic?.state,
|
|
39
|
+
metrics: event.payload.dynamic?.metrics,
|
|
40
|
+
doneCriteria: event.payload.dynamic?.doneCriteria,
|
|
41
|
+
iteration: event.payload.dynamic?.iteration ?? 0,
|
|
42
|
+
nextWakeAt: event.payload.dynamic?.nextWakeAt,
|
|
43
|
+
awaitingUpdate: event.payload.dynamic?.awaitingUpdate ?? false,
|
|
44
|
+
lastUpdatedAt: event.payload.dynamic?.lastUpdatedAt ?? event.at,
|
|
45
|
+
}
|
|
46
|
+
: undefined,
|
|
35
47
|
};
|
|
36
48
|
next.loopsById[id] = loop;
|
|
37
49
|
return {
|
|
@@ -68,6 +80,20 @@ export function reduceLoopState(state, event) {
|
|
|
68
80
|
loop.fireCount = (loop.fireCount ?? 0) + 1;
|
|
69
81
|
loop.updatedAt = event.at;
|
|
70
82
|
}
|
|
83
|
+
if (event.type === "LOOP_DYNAMIC_UPDATED") {
|
|
84
|
+
loop.prompt = event.payload.prompt ?? loop.prompt;
|
|
85
|
+
loop.dynamic = {
|
|
86
|
+
goal: event.payload.dynamic.goal ?? loop.dynamic?.goal ?? loop.prompt,
|
|
87
|
+
state: event.payload.dynamic.state ?? loop.dynamic?.state,
|
|
88
|
+
metrics: event.payload.dynamic.metrics ?? loop.dynamic?.metrics,
|
|
89
|
+
doneCriteria: event.payload.dynamic.doneCriteria ?? loop.dynamic?.doneCriteria,
|
|
90
|
+
iteration: event.payload.dynamic.iteration ?? loop.dynamic?.iteration ?? 0,
|
|
91
|
+
nextWakeAt: "nextWakeAt" in event.payload.dynamic ? event.payload.dynamic.nextWakeAt : loop.dynamic?.nextWakeAt,
|
|
92
|
+
awaitingUpdate: event.payload.dynamic.awaitingUpdate ?? loop.dynamic?.awaitingUpdate ?? false,
|
|
93
|
+
lastUpdatedAt: event.payload.dynamic.lastUpdatedAt ?? event.at,
|
|
94
|
+
};
|
|
95
|
+
loop.updatedAt = event.at;
|
|
96
|
+
}
|
|
71
97
|
next.loopsById[id] = loop;
|
|
72
98
|
return {
|
|
73
99
|
state: next,
|
package/dist/monitor-manager.js
CHANGED
|
@@ -246,7 +246,7 @@ export class MonitorManager {
|
|
|
246
246
|
const bp = this.processes.get(id);
|
|
247
247
|
if (!bp)
|
|
248
248
|
return false;
|
|
249
|
-
if (bp.entry.status === "completed") {
|
|
249
|
+
if (bp.entry.status === "completed" || bp.entry.status === "error") {
|
|
250
250
|
callback();
|
|
251
251
|
return true;
|
|
252
252
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DynamicLoopState } from "./types.js";
|
|
1
2
|
type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
2
3
|
export interface ReducerNotification {
|
|
3
4
|
key: string;
|
|
@@ -8,6 +9,7 @@ export interface ReducerNotification {
|
|
|
8
9
|
recurring?: boolean;
|
|
9
10
|
autoTask?: boolean;
|
|
10
11
|
readOnly?: boolean;
|
|
12
|
+
dynamic?: DynamicLoopState;
|
|
11
13
|
}
|
|
12
14
|
export interface NotificationReducerState {
|
|
13
15
|
notificationsByKey: Record<string, ReducerNotification>;
|
|
@@ -25,7 +25,7 @@ export interface ReducerBackedStoreConfig<TEntry, TState, TEvent, TData> {
|
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Shared persistence + reducer-dispatch machinery for the file-backed entity
|
|
28
|
-
* stores (loops, tasks
|
|
28
|
+
* stores (loops, tasks). Owns file locking, signature-gated load, atomic
|
|
29
29
|
* save, and reducer application; subclasses add only their entity-specific
|
|
30
30
|
* command methods.
|
|
31
31
|
*
|
|
@@ -48,7 +48,7 @@ function isProcessRunning(pid) {
|
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* Shared persistence + reducer-dispatch machinery for the file-backed entity
|
|
51
|
-
* stores (loops, tasks
|
|
51
|
+
* stores (loops, tasks). Owns file locking, signature-gated load, atomic
|
|
52
52
|
* save, and reducer application; subclasses add only their entity-specific
|
|
53
53
|
* command methods.
|
|
54
54
|
*
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Cross-extension RPC channels served by a tasks provider (pi-tasks or pi-loop native). */
|
|
2
|
+
export declare const TASKS_RPC: {
|
|
3
|
+
readonly ping: "tasks:rpc:ping";
|
|
4
|
+
readonly create: "tasks:rpc:create";
|
|
5
|
+
readonly update: "tasks:rpc:update";
|
|
6
|
+
readonly pending: "tasks:rpc:pending";
|
|
7
|
+
readonly clean: "tasks:rpc:clean";
|
|
8
|
+
};
|
|
9
|
+
/** Cross-extension RPC channels served by @tintinweb/pi-subagents. */
|
|
10
|
+
export declare const SUBAGENTS_RPC: {
|
|
11
|
+
readonly ping: "subagents:rpc:ping";
|
|
12
|
+
readonly spawn: "subagents:rpc:spawn";
|
|
13
|
+
readonly stop: "subagents:rpc:stop";
|
|
14
|
+
};
|
|
15
|
+
/** Broadcast (fire-and-forget) task lifecycle events. */
|
|
16
|
+
export declare const TASK_EVENTS: {
|
|
17
|
+
readonly ready: "tasks:ready";
|
|
18
|
+
readonly created: "tasks:created";
|
|
19
|
+
readonly started: "tasks:started";
|
|
20
|
+
readonly completed: "tasks:completed";
|
|
21
|
+
readonly reopened: "tasks:reopened";
|
|
22
|
+
readonly updated: "tasks:updated";
|
|
23
|
+
readonly deleted: "tasks:deleted";
|
|
24
|
+
};
|
|
25
|
+
export declare function replyChannel(channel: string, requestId: string): string;
|
|
26
|
+
export type TaskStatusWire = "pending" | "in_progress" | "completed";
|
|
27
|
+
export interface TaskEntryWire {
|
|
28
|
+
id: string;
|
|
29
|
+
subject: string;
|
|
30
|
+
description: string;
|
|
31
|
+
status: TaskStatusWire;
|
|
32
|
+
createdAt: number;
|
|
33
|
+
updatedAt: number;
|
|
34
|
+
completedAt?: number;
|
|
35
|
+
metadata?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
export interface CreateTaskParams {
|
|
38
|
+
subject: string;
|
|
39
|
+
description: string;
|
|
40
|
+
metadata?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
export interface CreateTaskReply {
|
|
43
|
+
id: string;
|
|
44
|
+
task: TaskEntryWire;
|
|
45
|
+
}
|
|
46
|
+
export interface UpdateTaskParams {
|
|
47
|
+
id: string;
|
|
48
|
+
status?: TaskStatusWire;
|
|
49
|
+
subject?: string;
|
|
50
|
+
description?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface UpdateTaskReply {
|
|
53
|
+
task: TaskEntryWire;
|
|
54
|
+
}
|
|
55
|
+
export interface PendingReply {
|
|
56
|
+
pending: number;
|
|
57
|
+
}
|
|
58
|
+
export interface CleanReply {
|
|
59
|
+
pruned: number;
|
|
60
|
+
}
|
|
61
|
+
export interface PingReply {
|
|
62
|
+
version: number;
|
|
63
|
+
/** Identifies which extension answered; lets a provider ignore its own reply. */
|
|
64
|
+
provider?: string;
|
|
65
|
+
}
|
|
66
|
+
export interface SpawnParams {
|
|
67
|
+
type: string;
|
|
68
|
+
prompt: string;
|
|
69
|
+
options?: Record<string, unknown>;
|
|
70
|
+
}
|
|
71
|
+
export interface SpawnReply {
|
|
72
|
+
id: string;
|
|
73
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// VENDORED MODULE — canonical copy shared verbatim by pi-loop and pi-orca.
|
|
2
|
+
// If you edit this file, copy it to the sibling repo and bump VENDOR_REV.
|
|
3
|
+
// VENDOR_REV: 1
|
|
4
|
+
/** Cross-extension RPC channels served by a tasks provider (pi-tasks or pi-loop native). */
|
|
5
|
+
export const TASKS_RPC = {
|
|
6
|
+
ping: "tasks:rpc:ping",
|
|
7
|
+
create: "tasks:rpc:create",
|
|
8
|
+
update: "tasks:rpc:update",
|
|
9
|
+
pending: "tasks:rpc:pending",
|
|
10
|
+
clean: "tasks:rpc:clean",
|
|
11
|
+
};
|
|
12
|
+
/** Cross-extension RPC channels served by @tintinweb/pi-subagents. */
|
|
13
|
+
export const SUBAGENTS_RPC = {
|
|
14
|
+
ping: "subagents:rpc:ping",
|
|
15
|
+
spawn: "subagents:rpc:spawn",
|
|
16
|
+
stop: "subagents:rpc:stop",
|
|
17
|
+
};
|
|
18
|
+
/** Broadcast (fire-and-forget) task lifecycle events. */
|
|
19
|
+
export const TASK_EVENTS = {
|
|
20
|
+
ready: "tasks:ready",
|
|
21
|
+
created: "tasks:created",
|
|
22
|
+
started: "tasks:started",
|
|
23
|
+
completed: "tasks:completed",
|
|
24
|
+
reopened: "tasks:reopened",
|
|
25
|
+
updated: "tasks:updated",
|
|
26
|
+
deleted: "tasks:deleted",
|
|
27
|
+
};
|
|
28
|
+
export function replyChannel(channel, requestId) {
|
|
29
|
+
return `${channel}:reply:${requestId}`;
|
|
30
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type PingReply } from "./channels.js";
|
|
2
|
+
/**
|
|
3
|
+
* Cross-extension request/reply over the in-process pi event bus.
|
|
4
|
+
*
|
|
5
|
+
* Wire contract (matches the pi-mono convention used by @tintinweb/pi-subagents):
|
|
6
|
+
* request: emit(channel, { requestId, ...params })
|
|
7
|
+
* reply: emit(`${channel}:reply:${requestId}`, RpcReply)
|
|
8
|
+
*/
|
|
9
|
+
export declare const PROTOCOL_VERSION = 2;
|
|
10
|
+
export type RpcReply<T = void> = {
|
|
11
|
+
success: true;
|
|
12
|
+
data?: T;
|
|
13
|
+
} | {
|
|
14
|
+
success: false;
|
|
15
|
+
error: string;
|
|
16
|
+
};
|
|
17
|
+
export declare class RpcError extends Error {
|
|
18
|
+
readonly channel: string;
|
|
19
|
+
readonly timedOut: boolean;
|
|
20
|
+
constructor(channel: string, message: string, timedOut?: boolean);
|
|
21
|
+
}
|
|
22
|
+
/** Minimal event-bus surface — satisfied by pi.events. */
|
|
23
|
+
export interface RpcEventBus {
|
|
24
|
+
on(event: string, handler: (data: unknown) => void): () => void;
|
|
25
|
+
emit(event: string, data: unknown): void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Call a remote handler and await its reply.
|
|
29
|
+
*
|
|
30
|
+
* Rejects with RpcError on a failure envelope or on timeout (timedOut=true).
|
|
31
|
+
* Callers that treat failure as "fall back" wrap this in try/catch at the
|
|
32
|
+
* layer that knows the fallback — the RPC layer itself never returns sentinels.
|
|
33
|
+
*/
|
|
34
|
+
export declare function rpcCall<T = void>(bus: RpcEventBus, channel: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
|
|
35
|
+
/**
|
|
36
|
+
* Detection probe: resolves undefined when nobody answers the ping channel.
|
|
37
|
+
* Absence of a provider is expected, not an error.
|
|
38
|
+
*
|
|
39
|
+
* First reply wins. If the probing extension also serves this channel, its
|
|
40
|
+
* own reply will settle the probe — in that case listen on the reply channel
|
|
41
|
+
* directly for the full window and filter by PingReply.provider instead.
|
|
42
|
+
*/
|
|
43
|
+
export declare function rpcProbe(bus: RpcEventBus, pingChannel: string, timeoutMs?: number): Promise<PingReply | undefined>;
|
|
44
|
+
export interface HandleRpcOptions {
|
|
45
|
+
/**
|
|
46
|
+
* When false, the handler is a silent no-op: another extension owns the
|
|
47
|
+
* channel and will reply; emitting a failure here would race its reply.
|
|
48
|
+
*/
|
|
49
|
+
enabled?: () => boolean;
|
|
50
|
+
debug?: (...args: unknown[]) => void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Register a server handler for one RPC channel. Returns the unsubscribe fn.
|
|
54
|
+
*
|
|
55
|
+
* Malformed requests get a failure reply, never a silent drop — the only
|
|
56
|
+
* silent case is a missing requestId, which leaves no reply address.
|
|
57
|
+
* A thrown/rejected fn becomes a failure envelope.
|
|
58
|
+
*/
|
|
59
|
+
export declare function handleRpc<P extends object, R = unknown>(bus: RpcEventBus, channel: string, fn: (params: P) => R | Promise<R>, opts?: HandleRpcOptions): () => void;
|