@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.
Files changed (45) hide show
  1. package/README.md +11 -1
  2. package/dist/coordinator.d.ts +35 -0
  3. package/dist/coordinator.js +56 -0
  4. package/dist/goal-types.d.ts +78 -0
  5. package/dist/goal-types.js +1 -0
  6. package/dist/goal-verifier.d.ts +20 -0
  7. package/dist/goal-verifier.js +198 -0
  8. package/dist/index.js +182 -40
  9. package/dist/loop-reducer.d.ts +63 -0
  10. package/dist/loop-reducer.js +67 -0
  11. package/dist/monitor-completion-coordinator.d.ts +10 -0
  12. package/dist/monitor-completion-coordinator.js +13 -0
  13. package/dist/monitor-manager.d.ts +2 -0
  14. package/dist/monitor-manager.js +107 -29
  15. package/dist/monitor-reducer.d.ts +82 -0
  16. package/dist/monitor-reducer.js +69 -0
  17. package/dist/notification-reducer.d.ts +81 -0
  18. package/dist/notification-reducer.js +65 -0
  19. package/dist/store.d.ts +2 -0
  20. package/dist/store.js +118 -44
  21. package/dist/task-backlog-coordinator.d.ts +12 -0
  22. package/dist/task-backlog-coordinator.js +22 -0
  23. package/dist/task-reducer.d.ts +66 -0
  24. package/dist/task-reducer.js +76 -0
  25. package/dist/task-store.d.ts +2 -0
  26. package/dist/task-store.js +82 -30
  27. package/docs/architecture/goal-state-schema.md +505 -0
  28. package/docs/architecture/state-machine-migration.md +546 -0
  29. package/docs/architecture/state-machine-reducer-event-model.md +823 -0
  30. package/docs/architecture/state-machine-test-matrix.md +249 -0
  31. package/docs/architecture/state-machine-transition-map.md +436 -0
  32. package/package.json +1 -1
  33. package/src/coordinator.ts +115 -0
  34. package/src/goal-types.ts +99 -0
  35. package/src/goal-verifier.ts +241 -0
  36. package/src/index.ts +209 -39
  37. package/src/loop-reducer.ts +148 -0
  38. package/src/monitor-completion-coordinator.ts +24 -0
  39. package/src/monitor-manager.ts +115 -27
  40. package/src/monitor-reducer.ts +166 -0
  41. package/src/notification-reducer.ts +155 -0
  42. package/src/store.ts +119 -43
  43. package/src/task-backlog-coordinator.ts +32 -0
  44. package/src/task-reducer.ts +152 -0
  45. package/src/task-store.ts +84 -27
package/dist/index.js CHANGED
@@ -16,10 +16,14 @@
16
16
  import { randomUUID } from "node:crypto";
17
17
  import { join, resolve } from "node:path";
18
18
  import { Type } from "typebox";
19
+ import { createCoordinator, } from "./coordinator.js";
19
20
  import { parseInterval } from "./loop-parse.js";
21
+ import { reduceMonitorCompletionEvent, } from "./monitor-completion-coordinator.js";
20
22
  import { MonitorManager } from "./monitor-manager.js";
23
+ import { reduceNotificationState, } from "./notification-reducer.js";
21
24
  import { CronScheduler } from "./scheduler.js";
22
25
  import { LoopStore } from "./store.js";
26
+ import { reduceTaskBacklogEvent, } from "./task-backlog-coordinator.js";
23
27
  import { TaskStore } from "./task-store.js";
24
28
  import { TriggerSystem } from "./trigger-system.js";
25
29
  import { LoopWidget } from "./ui/widget.js";
@@ -183,12 +187,89 @@ export default function (pi) {
183
187
  if (nativeTaskStore) {
184
188
  nativeTaskStore.sweepCompleted();
185
189
  widget.update();
186
- await cleanupTaskBacklogLoops();
190
+ await evaluateTaskBacklog(nativeTaskStore, nativeTaskStore.pendingCount());
187
191
  }
188
192
  }
189
- let agentRunning = false;
190
- const pendingNotifications = new Map();
193
+ let notificationState = {
194
+ notificationsByKey: {},
195
+ agentRunning: false,
196
+ hasPendingMessages: false,
197
+ };
191
198
  let flushPromise;
199
+ let notificationCoordinatorDelivered = false;
200
+ let notificationCoordinatorDeliveredSuccessfully = false;
201
+ const notificationReducerHandler = (incoming) => {
202
+ const result = reduceNotificationState(notificationState, incoming);
203
+ notificationState = result.state;
204
+ return result.effects;
205
+ };
206
+ const notificationCoordinator = createCoordinator({
207
+ reducers: [notificationReducerHandler],
208
+ effectHandlers: {
209
+ REQUEST_NOTIFICATION_FLUSH: () => { },
210
+ DELIVER_NOTIFICATION: async (effect) => {
211
+ notificationCoordinatorDelivered = true;
212
+ notificationCoordinatorDeliveredSuccessfully = await deliverNotification(effect.payload.notification);
213
+ },
214
+ },
215
+ });
216
+ const monitorCompletionReducerHandler = (incoming) => {
217
+ if (incoming.type !== "MONITOR_ONDONE_TRIGGERED")
218
+ return [];
219
+ return reduceMonitorCompletionEvent(incoming);
220
+ };
221
+ const monitorCompletionCoordinator = createCoordinator({
222
+ reducers: [monitorCompletionReducerHandler],
223
+ effectHandlers: {
224
+ DELIVER_MONITOR_ONDONE_WAKE: (effect) => {
225
+ const { loopId, monitorId } = effect.payload;
226
+ const current = store.get(loopId);
227
+ if (!current)
228
+ return;
229
+ debug(`onDone loop #${loopId} — monitor #${monitorId} completed, delivering through coordinator`);
230
+ onLoopFire(current);
231
+ store.delete(loopId);
232
+ },
233
+ },
234
+ });
235
+ let taskBacklogCoordinatorStore;
236
+ let taskBacklogCoordinatorWorker = { created: false };
237
+ let taskBacklogCoordinatorCleanupCount = 0;
238
+ const taskBacklogReducerHandler = (incoming) => {
239
+ if (incoming.type !== "TASK_BACKLOG_EVALUATED")
240
+ return [];
241
+ return reduceTaskBacklogEvent(incoming);
242
+ };
243
+ const taskBacklogCoordinator = createCoordinator({
244
+ reducers: [taskBacklogReducerHandler],
245
+ effectHandlers: {
246
+ ENSURE_AUTO_TASK_WORKER: async () => {
247
+ if (!taskBacklogCoordinatorStore)
248
+ return;
249
+ taskBacklogCoordinatorWorker = await ensureAutoTaskWorkerLoop(taskBacklogCoordinatorStore);
250
+ },
251
+ CLEANUP_TASK_BACKLOG_LOOPS: async () => {
252
+ taskBacklogCoordinatorCleanupCount = await cleanupTaskBacklogLoops();
253
+ },
254
+ },
255
+ });
256
+ function applyNotificationEvent(event) {
257
+ const result = reduceNotificationState(notificationState, event);
258
+ notificationState = result.state;
259
+ return result;
260
+ }
261
+ function syncNotificationRuntimeState(options) {
262
+ applyNotificationEvent({
263
+ type: "NOTIFICATION_RUNTIME_UPDATED",
264
+ at: Date.now(),
265
+ source: "system",
266
+ entityType: "notification",
267
+ payload: {
268
+ agentRunning: options?.agentRunning ?? notificationState.agentRunning,
269
+ hasPendingMessages: options?.hasPendingMessages ?? (_latestCtx?.hasPendingMessages() ?? false),
270
+ },
271
+ });
272
+ }
192
273
  function buildLoopFireMessage(data) {
193
274
  const triggerInfo = typeof data.trigger === "string"
194
275
  ? data.trigger
@@ -253,7 +334,7 @@ export default function (pi) {
253
334
  return false;
254
335
  }
255
336
  }
256
- agentRunning = true;
337
+ syncNotificationRuntimeState({ agentRunning: true });
257
338
  pi.sendMessage({
258
339
  customType: "pi-loop",
259
340
  content: notification.message,
@@ -276,16 +357,22 @@ export default function (pi) {
276
357
  if (flushPromise)
277
358
  return flushPromise;
278
359
  flushPromise = (async () => {
279
- if (agentRunning)
280
- return;
281
- if (!options?.ignorePendingMessages && _latestCtx?.hasPendingMessages())
282
- return;
283
- const entries = [...pendingNotifications.entries()]
284
- .sort(([, left], [, right]) => left.timestamp - right.timestamp);
285
- for (const [key, notification] of entries) {
286
- pendingNotifications.delete(key);
287
- const delivered = await deliverNotification(notification);
288
- if (delivered)
360
+ syncNotificationRuntimeState({
361
+ hasPendingMessages: _latestCtx?.hasPendingMessages() ?? false,
362
+ });
363
+ while (true) {
364
+ notificationCoordinatorDelivered = false;
365
+ notificationCoordinatorDeliveredSuccessfully = false;
366
+ await notificationCoordinator.dispatch({
367
+ type: "NOTIFICATION_FLUSH_REQUESTED",
368
+ at: Date.now(),
369
+ source: "system",
370
+ entityType: "notification",
371
+ payload: { ignorePendingMessages: options?.ignorePendingMessages },
372
+ });
373
+ if (!notificationCoordinatorDelivered)
374
+ return;
375
+ if (notificationCoordinatorDeliveredSuccessfully)
289
376
  return;
290
377
  }
291
378
  })().finally(() => {
@@ -295,7 +382,14 @@ export default function (pi) {
295
382
  }
296
383
  async function queueOrDeliverNotification(data) {
297
384
  const notification = buildPendingNotification(data);
298
- pendingNotifications.set(notification.key, notification);
385
+ applyNotificationEvent({
386
+ type: "NOTIFICATION_QUEUED",
387
+ at: notification.timestamp,
388
+ source: "system",
389
+ entityType: "notification",
390
+ entityId: notification.key,
391
+ payload: { notification },
392
+ });
299
393
  await flushPendingNotifications();
300
394
  }
301
395
  // ── Loop fire handler ──
@@ -370,28 +464,40 @@ export default function (pi) {
370
464
  widget.update();
371
465
  });
372
466
  pi.on("agent_start", async (_event, ctx) => {
373
- agentRunning = true;
467
+ syncNotificationRuntimeState({ agentRunning: true, hasPendingMessages: ctx.hasPendingMessages() });
374
468
  _latestCtx = ctx;
375
469
  widget.setUICtx(ctx.ui);
376
470
  });
377
471
  pi.on("agent_end", async (_event, ctx) => {
378
- agentRunning = false;
379
472
  _latestCtx = ctx;
380
473
  widget.setUICtx(ctx.ui);
474
+ syncNotificationRuntimeState({ agentRunning: false, hasPendingMessages: ctx.hasPendingMessages() });
381
475
  await flushPendingNotifications({ ignorePendingMessages: true });
382
476
  await cleanupTaskBacklogLoops();
383
477
  await pumpLoops();
384
478
  });
385
479
  pi.on("session_shutdown", async () => {
386
- agentRunning = false;
387
- pendingNotifications.clear();
480
+ syncNotificationRuntimeState({ agentRunning: false, hasPendingMessages: false });
481
+ applyNotificationEvent({
482
+ type: "NOTIFICATION_CLEARED",
483
+ at: Date.now(),
484
+ source: "session",
485
+ entityType: "notification",
486
+ payload: { reason: "session_shutdown" },
487
+ });
388
488
  });
389
489
  pi.on("session_switch", async (event, ctx) => {
390
490
  _latestCtx = ctx;
391
491
  widget.setUICtx(ctx.ui);
392
492
  triggerSystem.stop();
393
- agentRunning = false;
394
- pendingNotifications.clear();
493
+ syncNotificationRuntimeState({ agentRunning: false, hasPendingMessages: false });
494
+ applyNotificationEvent({
495
+ type: "NOTIFICATION_CLEARED",
496
+ at: Date.now(),
497
+ source: "session",
498
+ entityType: "notification",
499
+ payload: { reason: "session_switch" },
500
+ });
395
501
  _sessionId = undefined;
396
502
  const isResume = event?.reason === "resume";
397
503
  storeUpgraded = false;
@@ -403,6 +509,18 @@ export default function (pi) {
403
509
  showPersistedLoops(isResume);
404
510
  widget.update();
405
511
  });
512
+ pi.on("tool_execution_end", async (event, ctx) => {
513
+ _latestCtx = ctx;
514
+ widget.setUICtx(ctx.ui);
515
+ if (event?.toolName !== "bash" || event?.isError)
516
+ return;
517
+ const command = event?.args?.command ?? event?.input?.command;
518
+ if (typeof command !== "string")
519
+ return;
520
+ if (!/^\s*git\s+commit\b/i.test(command))
521
+ return;
522
+ await cleanDoneTasks();
523
+ });
406
524
  // ── Dynamic loop pump — fires cron/hybrid loops on idle instead of wall-clock timers ──
407
525
  async function pumpLoops() {
408
526
  const pendingTasks = new Map();
@@ -572,12 +690,14 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
572
690
  });
573
691
  function handleMonitorDoneLoop(doneLoop, monitorId) {
574
692
  const deliver = () => {
575
- const current = store.get(doneLoop.id);
576
- if (!current)
577
- return;
578
- debug(`onDone loop #${doneLoop.id} — monitor #${monitorId} completed, delivering directly`);
579
- onLoopFire(current);
580
- store.delete(doneLoop.id);
693
+ void monitorCompletionCoordinator.dispatch({
694
+ type: "MONITOR_ONDONE_TRIGGERED",
695
+ at: Date.now(),
696
+ source: "monitor",
697
+ entityType: "monitor",
698
+ entityId: monitorId,
699
+ payload: { loopId: doneLoop.id, monitorId },
700
+ });
581
701
  };
582
702
  const registered = monitorManager.onComplete(monitorId, deliver);
583
703
  if (registered)
@@ -1000,6 +1120,25 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
1000
1120
  widget.update();
1001
1121
  return { entry, created: true };
1002
1122
  }
1123
+ async function evaluateTaskBacklog(taskStore, pendingCount) {
1124
+ const resolvedPending = pendingCount ?? (taskStore ? taskStore.pendingCount() : await hasPendingTasks());
1125
+ taskBacklogCoordinatorStore = taskStore;
1126
+ taskBacklogCoordinatorWorker = { created: false };
1127
+ taskBacklogCoordinatorCleanupCount = 0;
1128
+ await taskBacklogCoordinator.dispatch({
1129
+ type: "TASK_BACKLOG_EVALUATED",
1130
+ at: Date.now(),
1131
+ source: "system",
1132
+ entityType: "task",
1133
+ payload: { pendingCount: resolvedPending, threshold: AUTO_TASK_WORKER_THRESHOLD },
1134
+ });
1135
+ taskBacklogCoordinatorStore = undefined;
1136
+ return {
1137
+ entry: taskBacklogCoordinatorWorker.entry,
1138
+ created: taskBacklogCoordinatorWorker.created,
1139
+ cleaned: taskBacklogCoordinatorCleanupCount,
1140
+ };
1141
+ }
1003
1142
  async function createNativeTaskInteractively(ui) {
1004
1143
  if (!nativeTaskStore) {
1005
1144
  ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
@@ -1016,11 +1155,11 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
1016
1155
  description: entry.description,
1017
1156
  status: entry.status,
1018
1157
  });
1019
- const worker = await ensureAutoTaskWorkerLoop(nativeTaskStore);
1158
+ const backlog = await evaluateTaskBacklog(nativeTaskStore, nativeTaskStore.pendingCount());
1020
1159
  widget.update();
1021
1160
  ui.notify(`Task #${entry.id} created`, "info");
1022
- if (worker.created && worker.entry) {
1023
- ui.notify(`Worker loop #${worker.entry.id} auto-created`, "info");
1161
+ if (backlog.created && backlog.entry) {
1162
+ ui.notify(`Worker loop #${backlog.entry.id} auto-created`, "info");
1024
1163
  }
1025
1164
  }
1026
1165
  async function viewNativeTasks(ui) {
@@ -1081,7 +1220,7 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
1081
1220
  ui.notify(`Task #${task.id} reopened`, "info");
1082
1221
  }
1083
1222
  widget.update();
1084
- await cleanupTaskBacklogLoops();
1223
+ await evaluateTaskBacklog(nativeTaskStore, nativeTaskStore.pendingCount());
1085
1224
  return viewNativeTasks(ui);
1086
1225
  }
1087
1226
  // ── Native task tools (only when pi-tasks is absent) ──
@@ -1107,11 +1246,11 @@ Use MonitorList to find the monitor ID, then stop it with this tool.`,
1107
1246
  description: entry.description,
1108
1247
  status: entry.status,
1109
1248
  });
1110
- const worker = await ensureAutoTaskWorkerLoop(nativeTaskStore);
1249
+ const backlog = await evaluateTaskBacklog(nativeTaskStore, nativeTaskStore.pendingCount());
1111
1250
  widget.update();
1112
1251
  ctx.ui.notify(`Task #${entry.id} created`, "info");
1113
- if (worker.created && worker.entry) {
1114
- ctx.ui.notify(`Worker loop #${worker.entry.id} auto-created`, "info");
1252
+ if (backlog.created && backlog.entry) {
1253
+ ctx.ui.notify(`Worker loop #${backlog.entry.id} auto-created`, "info");
1115
1254
  }
1116
1255
  return;
1117
1256
  }
@@ -1144,10 +1283,10 @@ Fields:
1144
1283
  description: entry.description,
1145
1284
  status: entry.status,
1146
1285
  });
1147
- const worker = await ensureAutoTaskWorkerLoop(taskStore);
1286
+ const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
1148
1287
  widget.update();
1149
- const autoLoopMsg = worker.created && worker.entry
1150
- ? `\nWorker loop #${worker.entry.id} auto-created`
1288
+ const autoLoopMsg = backlog.created && backlog.entry
1289
+ ? `\nWorker loop #${backlog.entry.id} auto-created`
1151
1290
  : "";
1152
1291
  return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${autoLoopMsg}`));
1153
1292
  },
@@ -1204,9 +1343,12 @@ Parameters: id (required), status, subject, description`,
1204
1343
  if (!entry)
1205
1344
  return Promise.resolve(textResult(`Task #${id} not found`));
1206
1345
  widget.update();
1207
- await cleanupTaskBacklogLoops();
1346
+ const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
1208
1347
  const statusMsg = status ? ` → ${status}` : "";
1209
- return Promise.resolve(textResult(`Task #${id} updated${statusMsg}`));
1348
+ const autoLoopMsg = backlog.created && backlog.entry
1349
+ ? `\nWorker loop #${backlog.entry.id} auto-created`
1350
+ : "";
1351
+ return Promise.resolve(textResult(`Task #${id} updated${statusMsg}${autoLoopMsg}`));
1210
1352
  },
1211
1353
  });
1212
1354
  pi.registerTool({
@@ -1220,7 +1362,7 @@ Parameters: id (required), status, subject, description`,
1220
1362
  const deleted = taskStore.delete(params.id);
1221
1363
  widget.update();
1222
1364
  if (deleted) {
1223
- await cleanupTaskBacklogLoops();
1365
+ await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
1224
1366
  return Promise.resolve(textResult(`Task #${params.id} deleted`));
1225
1367
  }
1226
1368
  return Promise.resolve(textResult(`Task #${params.id} not found`));
@@ -0,0 +1,63 @@
1
+ import type { LoopEntry, Trigger } from "./types.js";
2
+ export declare const MAX_LOOP_EXPIRY_MS: number;
3
+ type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
4
+ export interface LoopReducerState {
5
+ nextId: number;
6
+ loopsById: Record<string, LoopEntry>;
7
+ }
8
+ export type LoopReducerEvent = {
9
+ type: "LOOP_CREATED";
10
+ at: number;
11
+ source: ReducerSource;
12
+ entityType?: "loop";
13
+ entityId?: string;
14
+ payload: {
15
+ prompt: string;
16
+ trigger: Trigger;
17
+ recurring: boolean;
18
+ autoTask?: boolean;
19
+ taskBacklog?: boolean;
20
+ readOnly?: boolean;
21
+ maxFires?: number;
22
+ };
23
+ } | {
24
+ type: "LOOP_PAUSED" | "LOOP_RESUMED" | "LOOP_FIRED" | "LOOP_DELETED" | "LOOP_MAX_FIRES_REACHED" | "LOOP_BACKLOG_EMPTY";
25
+ at: number;
26
+ source: ReducerSource;
27
+ entityType?: "loop";
28
+ entityId?: string;
29
+ payload: {
30
+ id: string;
31
+ };
32
+ } | {
33
+ type: "LOOP_EXPIRED";
34
+ at: number;
35
+ source: ReducerSource;
36
+ entityType?: "loop";
37
+ entityId?: string;
38
+ payload: {
39
+ id: string;
40
+ reason: "expires_at" | "resume_event_stale" | "already_completed_monitor";
41
+ };
42
+ };
43
+ export type LoopReducerEffect = {
44
+ type: "PERSIST_LOOP";
45
+ entityType: "loop";
46
+ entityId: string;
47
+ payload: {
48
+ loop: LoopEntry;
49
+ };
50
+ } | {
51
+ type: "DELETE_LOOP";
52
+ entityType: "loop";
53
+ entityId: string;
54
+ payload: {
55
+ id: string;
56
+ };
57
+ };
58
+ export interface LoopReduceResult {
59
+ state: LoopReducerState;
60
+ effects: LoopReducerEffect[];
61
+ }
62
+ export declare function reduceLoopState(state: LoopReducerState, event: LoopReducerEvent): LoopReduceResult;
63
+ export {};
@@ -0,0 +1,67 @@
1
+ export const MAX_LOOP_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000;
2
+ function cloneState(state) {
3
+ return {
4
+ nextId: state.nextId,
5
+ loopsById: { ...state.loopsById },
6
+ };
7
+ }
8
+ export function reduceLoopState(state, event) {
9
+ if (event.type === "LOOP_CREATED") {
10
+ const next = cloneState(state);
11
+ const id = String(next.nextId++);
12
+ const loop = {
13
+ id,
14
+ prompt: event.payload.prompt,
15
+ trigger: event.payload.trigger,
16
+ status: "active",
17
+ recurring: event.payload.recurring,
18
+ createdAt: event.at,
19
+ updatedAt: event.at,
20
+ expiresAt: event.at + MAX_LOOP_EXPIRY_MS,
21
+ autoTask: event.payload.autoTask,
22
+ taskBacklog: event.payload.taskBacklog,
23
+ readOnly: event.payload.readOnly,
24
+ maxFires: event.payload.maxFires,
25
+ fireCount: 0,
26
+ };
27
+ next.loopsById[id] = loop;
28
+ return {
29
+ state: next,
30
+ effects: [{ type: "PERSIST_LOOP", entityType: "loop", entityId: id, payload: { loop } }],
31
+ };
32
+ }
33
+ const id = event.payload.id;
34
+ const current = state.loopsById[id];
35
+ if (!current)
36
+ return { state, effects: [] };
37
+ if (event.type === "LOOP_DELETED"
38
+ || event.type === "LOOP_MAX_FIRES_REACHED"
39
+ || event.type === "LOOP_EXPIRED"
40
+ || event.type === "LOOP_BACKLOG_EMPTY") {
41
+ const next = cloneState(state);
42
+ delete next.loopsById[id];
43
+ return {
44
+ state: next,
45
+ effects: [{ type: "DELETE_LOOP", entityType: "loop", entityId: id, payload: { id } }],
46
+ };
47
+ }
48
+ const next = cloneState(state);
49
+ const loop = { ...current };
50
+ if (event.type === "LOOP_PAUSED") {
51
+ loop.status = "paused";
52
+ loop.updatedAt = event.at;
53
+ }
54
+ if (event.type === "LOOP_RESUMED") {
55
+ loop.status = "active";
56
+ loop.updatedAt = event.at;
57
+ }
58
+ if (event.type === "LOOP_FIRED") {
59
+ loop.fireCount = (loop.fireCount ?? 0) + 1;
60
+ loop.updatedAt = event.at;
61
+ }
62
+ next.loopsById[id] = loop;
63
+ return {
64
+ state: next,
65
+ effects: [{ type: "PERSIST_LOOP", entityType: "loop", entityId: id, payload: { loop } }],
66
+ };
67
+ }
@@ -0,0 +1,10 @@
1
+ import type { ReducerEffect, ReducerEvent } from "./coordinator.js";
2
+ export type MonitorCompletionEvent = ReducerEvent<"MONITOR_ONDONE_TRIGGERED", {
3
+ loopId: string;
4
+ monitorId: string;
5
+ }>;
6
+ export type MonitorCompletionEffect = ReducerEffect<"DELIVER_MONITOR_ONDONE_WAKE", {
7
+ loopId: string;
8
+ monitorId: string;
9
+ }>;
10
+ export declare function reduceMonitorCompletionEvent(event: MonitorCompletionEvent): MonitorCompletionEffect[];
@@ -0,0 +1,13 @@
1
+ export function reduceMonitorCompletionEvent(event) {
2
+ if (event.type !== "MONITOR_ONDONE_TRIGGERED")
3
+ return [];
4
+ return [{
5
+ type: "DELIVER_MONITOR_ONDONE_WAKE",
6
+ entityType: "monitor",
7
+ entityId: event.payload.monitorId,
8
+ payload: {
9
+ loopId: event.payload.loopId,
10
+ monitorId: event.payload.monitorId,
11
+ },
12
+ }];
13
+ }
@@ -5,6 +5,8 @@ export declare class MonitorManager {
5
5
  private processes;
6
6
  private nextId;
7
7
  constructor(pi: ExtensionAPI);
8
+ private toReducerState;
9
+ private applyReducerEvent;
8
10
  create(command: string, description?: string, timeout?: number): MonitorEntry;
9
11
  get(id: string): MonitorEntry | undefined;
10
12
  list(): MonitorEntry[];