@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.
Files changed (152) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +64 -5
  3. package/dist/api.d.ts +11 -0
  4. package/dist/api.js +10 -0
  5. package/dist/commands/loop-command.d.ts +9 -2
  6. package/dist/commands/loop-command.js +81 -51
  7. package/dist/commands/tasks-command.js +3 -3
  8. package/dist/coordinator.d.ts +1 -1
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.js +89 -19
  11. package/dist/loop-format.d.ts +3 -0
  12. package/dist/loop-format.js +20 -0
  13. package/dist/loop-parse.d.ts +1 -0
  14. package/dist/loop-parse.js +59 -6
  15. package/dist/loop-reducer.d.ts +13 -1
  16. package/dist/loop-reducer.js +26 -0
  17. package/dist/monitor-manager.js +1 -1
  18. package/dist/notification-reducer.d.ts +2 -0
  19. package/dist/reducer-backed-store.d.ts +1 -1
  20. package/dist/reducer-backed-store.js +1 -1
  21. package/dist/rpc/channels.d.ts +73 -0
  22. package/dist/rpc/channels.js +30 -0
  23. package/dist/rpc/cross-extension-rpc.d.ts +59 -0
  24. package/dist/rpc/cross-extension-rpc.js +117 -0
  25. package/dist/runtime/loop-events.d.ts +26 -0
  26. package/dist/runtime/loop-events.js +26 -0
  27. package/dist/runtime/monitor-ondone-runtime.js +1 -1
  28. package/dist/runtime/native-task-rpc.d.ts +22 -0
  29. package/dist/runtime/native-task-rpc.js +50 -0
  30. package/dist/runtime/notification-runtime.d.ts +2 -1
  31. package/dist/runtime/notification-runtime.js +19 -7
  32. package/dist/runtime/task-backlog-runtime.d.ts +5 -1
  33. package/dist/runtime/task-backlog-runtime.js +14 -7
  34. package/dist/runtime/task-mutations.d.ts +39 -0
  35. package/dist/runtime/task-mutations.js +68 -0
  36. package/dist/runtime/task-rpc.d.ts +4 -0
  37. package/dist/runtime/task-rpc.js +40 -58
  38. package/dist/scheduler.js +25 -7
  39. package/dist/store.d.ts +9 -1
  40. package/dist/store.js +57 -0
  41. package/dist/tools/loop-tools.d.ts +8 -0
  42. package/dist/tools/loop-tools.js +105 -15
  43. package/dist/tools/monitor-tools.js +1 -3
  44. package/dist/tools/native-task-tools.d.ts +2 -6
  45. package/dist/tools/native-task-tools.js +25 -55
  46. package/dist/tools/tool-result.d.ts +8 -0
  47. package/dist/tools/tool-result.js +4 -0
  48. package/dist/trigger-system.js +1 -1
  49. package/dist/types.d.ts +24 -1
  50. package/package.json +15 -2
  51. package/src/api.ts +36 -0
  52. package/src/commands/loop-command.ts +100 -55
  53. package/src/commands/tasks-command.ts +3 -3
  54. package/src/coordinator.ts +1 -1
  55. package/src/index.ts +92 -19
  56. package/src/loop-format.ts +22 -0
  57. package/src/loop-parse.ts +62 -8
  58. package/src/loop-reducer.ts +41 -1
  59. package/src/monitor-manager.ts +1 -1
  60. package/src/notification-reducer.ts +3 -0
  61. package/src/reducer-backed-store.ts +3 -3
  62. package/src/rpc/channels.ts +97 -0
  63. package/src/rpc/cross-extension-rpc.ts +152 -0
  64. package/src/runtime/loop-events.ts +60 -0
  65. package/src/runtime/monitor-ondone-runtime.ts +1 -1
  66. package/src/runtime/native-task-rpc.ts +139 -0
  67. package/src/runtime/notification-runtime.ts +21 -8
  68. package/src/runtime/task-backlog-runtime.ts +25 -7
  69. package/src/runtime/task-mutations.ts +103 -0
  70. package/src/runtime/task-rpc.ts +48 -54
  71. package/src/scheduler.ts +25 -8
  72. package/src/store.ts +59 -2
  73. package/src/tools/loop-tools.ts +129 -16
  74. package/src/tools/monitor-tools.ts +1 -4
  75. package/src/tools/native-task-tools.ts +31 -52
  76. package/src/tools/tool-result.ts +4 -0
  77. package/src/trigger-system.ts +5 -5
  78. package/src/types.ts +29 -1
  79. package/.release-please-manifest.json +0 -3
  80. package/AGENTS.md +0 -83
  81. package/CONTRIBUTING.md +0 -69
  82. package/Dockerfile +0 -17
  83. package/benchmarks/experiment-sim.js +0 -30
  84. package/coverage/base.css +0 -224
  85. package/coverage/block-navigation.js +0 -87
  86. package/coverage/coverage-final.json +0 -33
  87. package/coverage/favicon.png +0 -0
  88. package/coverage/index.html +0 -176
  89. package/coverage/prettify.css +0 -1
  90. package/coverage/prettify.js +0 -2
  91. package/coverage/sort-arrow-sprite.png +0 -0
  92. package/coverage/sorter.js +0 -210
  93. package/coverage/src/commands/index.html +0 -131
  94. package/coverage/src/commands/loop-command.ts.html +0 -634
  95. package/coverage/src/commands/tasks-command.ts.html +0 -490
  96. package/coverage/src/coordinator.ts.html +0 -430
  97. package/coverage/src/goal-coordinator.ts.html +0 -259
  98. package/coverage/src/goal-reducer.ts.html +0 -982
  99. package/coverage/src/goal-store.ts.html +0 -742
  100. package/coverage/src/goal-verifier.ts.html +0 -808
  101. package/coverage/src/index.html +0 -386
  102. package/coverage/src/index.ts.html +0 -1111
  103. package/coverage/src/loop-parse.ts.html +0 -574
  104. package/coverage/src/loop-reducer.ts.html +0 -559
  105. package/coverage/src/monitor-completion-coordinator.ts.html +0 -157
  106. package/coverage/src/monitor-manager.ts.html +0 -940
  107. package/coverage/src/monitor-reducer.ts.html +0 -583
  108. package/coverage/src/notification-reducer.ts.html +0 -550
  109. package/coverage/src/reducer-backed-store.ts.html +0 -589
  110. package/coverage/src/runtime/index.html +0 -206
  111. package/coverage/src/runtime/monitor-ondone-runtime.ts.html +0 -310
  112. package/coverage/src/runtime/notification-runtime.ts.html +0 -721
  113. package/coverage/src/runtime/scope.ts.html +0 -196
  114. package/coverage/src/runtime/session-runtime.ts.html +0 -679
  115. package/coverage/src/runtime/task-backlog-runtime.ts.html +0 -574
  116. package/coverage/src/runtime/task-events.ts.html +0 -208
  117. package/coverage/src/runtime/task-rpc.ts.html +0 -541
  118. package/coverage/src/scheduler.ts.html +0 -400
  119. package/coverage/src/store.ts.html +0 -667
  120. package/coverage/src/task-backlog-coordinator.ts.html +0 -181
  121. package/coverage/src/task-reducer.ts.html +0 -550
  122. package/coverage/src/task-store.ts.html +0 -526
  123. package/coverage/src/tools/index.html +0 -146
  124. package/coverage/src/tools/loop-tools.ts.html +0 -1000
  125. package/coverage/src/tools/monitor-tools.ts.html +0 -547
  126. package/coverage/src/tools/native-task-tools.ts.html +0 -556
  127. package/coverage/src/trigger-system.ts.html +0 -547
  128. package/coverage/src/ui/index.html +0 -116
  129. package/coverage/src/ui/widget.ts.html +0 -292
  130. package/dist/goal-coordinator.d.ts +0 -22
  131. package/dist/goal-coordinator.js +0 -28
  132. package/dist/goal-reducer.d.ts +0 -98
  133. package/dist/goal-reducer.js +0 -190
  134. package/dist/goal-store.d.ts +0 -22
  135. package/dist/goal-store.js +0 -188
  136. package/dist/goal-types.d.ts +0 -82
  137. package/dist/goal-types.js +0 -1
  138. package/dist/goal-verifier.d.ts +0 -20
  139. package/dist/goal-verifier.js +0 -198
  140. package/docker-compose.yml +0 -12
  141. package/docs/architecture/goal-state-schema.md +0 -505
  142. package/docs/architecture/state-machine-migration.md +0 -546
  143. package/docs/architecture/state-machine-reducer-event-model.md +0 -823
  144. package/docs/architecture/state-machine-test-matrix.md +0 -249
  145. package/docs/architecture/state-machine-transition-map.md +0 -436
  146. package/release-please-config.json +0 -9
  147. package/src/goal-coordinator.ts +0 -58
  148. package/src/goal-reducer.ts +0 -299
  149. package/src/goal-store.ts +0 -219
  150. package/src/goal-types.ts +0 -104
  151. package/src/goal-verifier.ts +0 -241
  152. 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: /loop [interval] [prompt]
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
- if (!nativeTaskStore)
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
- const tasks = nativeTaskStore.list().filter(t => t.status === "pending" || t.status === "in_progress");
59
- const active = tasks.find(t => t.status === "in_progress");
60
- const next = tasks.find(t => t.status === "pending");
61
- const focus = active
62
- ? `active: ${active.subject.slice(0, 50)}`
63
- : next
64
- ? `next: ${next.subject.slice(0, 50)}`
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: tasks.length, focusText: focus };
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: entry.id,
182
- prompt: entry.prompt,
183
- trigger: entry.trigger,
184
- timestamp: Date.now(),
185
- readOnly: entry.readOnly,
186
- recurring: entry.recurring,
187
- autoTask: entry.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 = new TaskStore(resolveTaskStorePath(getScopeOptions(), _sessionId));
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,3 @@
1
+ import type { Trigger } from "./types.js";
2
+ export type TriggerFormatStyle = "command" | "create" | "list" | "notification";
3
+ export declare function formatTrigger(trigger: Trigger | string, style?: TriggerFormatStyle): string;
@@ -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
+ }
@@ -1,3 +1,4 @@
1
+ export declare function isValidCronExpression(expr: string): boolean;
1
2
  export declare function parseInterval(input: string): {
2
3
  cron: string;
3
4
  description: string;
@@ -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 (parts.length !== 5)
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)
@@ -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";
@@ -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,
@@ -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, goals). Owns file locking, signature-gated load, atomic
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, goals). Owns file locking, signature-gated load, atomic
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;