@trevonistrevon/pi-loop 0.4.11 → 0.5.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 (87) hide show
  1. package/README.md +20 -9
  2. package/dist/commands/loop-command.d.ts +22 -0
  3. package/dist/commands/loop-command.js +148 -0
  4. package/dist/commands/tasks-command.d.ts +15 -0
  5. package/dist/commands/tasks-command.js +117 -0
  6. package/dist/coordinator.d.ts +35 -0
  7. package/dist/coordinator.js +56 -0
  8. package/dist/goal-coordinator.d.ts +22 -0
  9. package/dist/goal-coordinator.js +28 -0
  10. package/dist/goal-reducer.d.ts +89 -0
  11. package/dist/goal-reducer.js +181 -0
  12. package/dist/goal-store.d.ts +31 -0
  13. package/dist/goal-store.js +298 -0
  14. package/dist/goal-types.d.ts +82 -0
  15. package/dist/goal-types.js +1 -0
  16. package/dist/goal-verifier.d.ts +20 -0
  17. package/dist/goal-verifier.js +198 -0
  18. package/dist/index.js +130 -1087
  19. package/dist/loop-reducer.d.ts +63 -0
  20. package/dist/loop-reducer.js +67 -0
  21. package/dist/monitor-completion-coordinator.d.ts +10 -0
  22. package/dist/monitor-completion-coordinator.js +13 -0
  23. package/dist/monitor-manager.d.ts +2 -0
  24. package/dist/monitor-manager.js +107 -29
  25. package/dist/monitor-reducer.d.ts +82 -0
  26. package/dist/monitor-reducer.js +69 -0
  27. package/dist/notification-reducer.d.ts +81 -0
  28. package/dist/notification-reducer.js +65 -0
  29. package/dist/runtime/monitor-ondone-runtime.d.ts +13 -0
  30. package/dist/runtime/monitor-ondone-runtime.js +49 -0
  31. package/dist/runtime/notification-runtime.d.ts +34 -0
  32. package/dist/runtime/notification-runtime.js +152 -0
  33. package/dist/runtime/scope.d.ts +8 -0
  34. package/dist/runtime/scope.js +33 -0
  35. package/dist/runtime/session-runtime.d.ts +39 -0
  36. package/dist/runtime/session-runtime.js +110 -0
  37. package/dist/runtime/task-backlog-runtime.d.ts +36 -0
  38. package/dist/runtime/task-backlog-runtime.js +105 -0
  39. package/dist/runtime/task-rpc.d.ts +19 -0
  40. package/dist/runtime/task-rpc.js +118 -0
  41. package/dist/store.d.ts +7 -4
  42. package/dist/store.js +129 -49
  43. package/dist/task-backlog-coordinator.d.ts +12 -0
  44. package/dist/task-backlog-coordinator.js +22 -0
  45. package/dist/task-reducer.d.ts +66 -0
  46. package/dist/task-reducer.js +76 -0
  47. package/dist/task-store.d.ts +8 -4
  48. package/dist/task-store.js +102 -33
  49. package/dist/tools/loop-tools.d.ts +41 -0
  50. package/dist/tools/loop-tools.js +241 -0
  51. package/dist/tools/monitor-tools.d.ts +25 -0
  52. package/dist/tools/monitor-tools.js +110 -0
  53. package/dist/tools/native-task-tools.d.ts +15 -0
  54. package/dist/tools/native-task-tools.js +127 -0
  55. package/docs/architecture/goal-state-schema.md +505 -0
  56. package/docs/architecture/state-machine-migration.md +546 -0
  57. package/docs/architecture/state-machine-reducer-event-model.md +823 -0
  58. package/docs/architecture/state-machine-test-matrix.md +249 -0
  59. package/docs/architecture/state-machine-transition-map.md +436 -0
  60. package/package.json +1 -1
  61. package/src/commands/loop-command.ts +184 -0
  62. package/src/commands/tasks-command.ts +135 -0
  63. package/src/coordinator.ts +115 -0
  64. package/src/goal-coordinator.ts +58 -0
  65. package/src/goal-reducer.ts +280 -0
  66. package/src/goal-store.ts +315 -0
  67. package/src/goal-types.ts +104 -0
  68. package/src/goal-verifier.ts +241 -0
  69. package/src/index.ts +134 -1147
  70. package/src/loop-reducer.ts +148 -0
  71. package/src/monitor-completion-coordinator.ts +24 -0
  72. package/src/monitor-manager.ts +115 -27
  73. package/src/monitor-reducer.ts +166 -0
  74. package/src/notification-reducer.ts +155 -0
  75. package/src/runtime/monitor-ondone-runtime.ts +75 -0
  76. package/src/runtime/notification-runtime.ts +212 -0
  77. package/src/runtime/scope.ts +37 -0
  78. package/src/runtime/session-runtime.ts +168 -0
  79. package/src/runtime/task-backlog-runtime.ts +163 -0
  80. package/src/runtime/task-rpc.ts +150 -0
  81. package/src/store.ts +132 -50
  82. package/src/task-backlog-coordinator.ts +32 -0
  83. package/src/task-reducer.ts +152 -0
  84. package/src/task-store.ts +103 -31
  85. package/src/tools/loop-tools.ts +304 -0
  86. package/src/tools/monitor-tools.ts +145 -0
  87. package/src/tools/native-task-tools.ts +144 -0
@@ -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[];
@@ -1,4 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
+ import { reduceMonitorState, } from "./monitor-reducer.js";
2
3
  export class MonitorManager {
3
4
  pi;
4
5
  processes = new Map();
@@ -6,18 +7,41 @@ export class MonitorManager {
6
7
  constructor(pi) {
7
8
  this.pi = pi;
8
9
  }
9
- create(command, description, timeout = 300000) {
10
- const id = String(this.nextId++);
11
- const entry = {
12
- id,
13
- command,
14
- description,
15
- timeout,
16
- status: "running",
17
- startedAt: Date.now(),
18
- outputLines: 0,
19
- outputBuffer: [],
10
+ toReducerState() {
11
+ return {
12
+ nextId: this.nextId,
13
+ monitorsById: Object.fromEntries(Array.from(this.processes.entries()).map(([id, process]) => [id, process.entry])),
20
14
  };
15
+ }
16
+ applyReducerEvent(event) {
17
+ const result = reduceMonitorState(this.toReducerState(), event);
18
+ this.nextId = result.state.nextId;
19
+ for (const [id, process] of [...this.processes.entries()]) {
20
+ const updated = result.state.monitorsById[id];
21
+ if (!updated) {
22
+ this.processes.delete(id);
23
+ continue;
24
+ }
25
+ process.entry = updated;
26
+ }
27
+ return result;
28
+ }
29
+ create(command, description, timeout = 300000) {
30
+ const now = Date.now();
31
+ const result = reduceMonitorState(this.toReducerState(), {
32
+ type: "MONITOR_CREATED",
33
+ at: now,
34
+ source: "tool",
35
+ entityType: "monitor",
36
+ payload: {
37
+ command,
38
+ description,
39
+ timeout,
40
+ },
41
+ });
42
+ this.nextId = result.state.nextId;
43
+ const id = String(this.nextId - 1);
44
+ const entry = result.state.monitorsById[id];
21
45
  const abortController = new AbortController();
22
46
  const child = spawn("sh", ["-c", command], {
23
47
  stdio: ["ignore", "pipe", "pipe"],
@@ -37,9 +61,14 @@ export class MonitorManager {
37
61
  for (const line of lines) {
38
62
  if (line.length === 0)
39
63
  continue;
40
- entry.outputLines++;
41
- if (entry.outputBuffer.length < 200)
42
- entry.outputBuffer.push(line);
64
+ this.applyReducerEvent({
65
+ type: "MONITOR_OUTPUT",
66
+ at: Date.now(),
67
+ source: "monitor",
68
+ entityType: "monitor",
69
+ entityId: id,
70
+ payload: { id, line },
71
+ });
43
72
  this.pi.events.emit("monitor:output", {
44
73
  monitorId: id,
45
74
  line,
@@ -52,9 +81,14 @@ export class MonitorManager {
52
81
  for (const line of lines) {
53
82
  if (line.length === 0)
54
83
  continue;
55
- entry.outputLines++;
56
- if (entry.outputBuffer.length < 200)
57
- entry.outputBuffer.push(line);
84
+ this.applyReducerEvent({
85
+ type: "MONITOR_OUTPUT",
86
+ at: Date.now(),
87
+ source: "monitor",
88
+ entityType: "monitor",
89
+ entityId: id,
90
+ payload: { id, line },
91
+ });
58
92
  this.pi.events.emit("monitor:output", {
59
93
  monitorId: id,
60
94
  line,
@@ -63,13 +97,22 @@ export class MonitorManager {
63
97
  }
64
98
  });
65
99
  const finish = (code, status) => {
66
- entry.status = status;
67
- entry.exitCode = code ?? undefined;
68
- entry.completedAt = Date.now();
100
+ this.applyReducerEvent({
101
+ type: status === "completed" ? "MONITOR_COMPLETED" : "MONITOR_ERRORED",
102
+ at: Date.now(),
103
+ source: "monitor",
104
+ entityType: "monitor",
105
+ entityId: id,
106
+ payload: {
107
+ id,
108
+ exitCode: code ?? undefined,
109
+ },
110
+ });
111
+ const current = this.get(id);
69
112
  this.pi.events.emit(status === "completed" ? "monitor:done" : "monitor:error", {
70
113
  monitorId: id,
71
114
  exitCode: code,
72
- outputLines: entry.outputLines,
115
+ outputLines: current.outputLines,
73
116
  });
74
117
  if (status === "completed") {
75
118
  for (const callback of bp.completionCallbacks)
@@ -81,17 +124,35 @@ export class MonitorManager {
81
124
  bp.waiters = [];
82
125
  // Remove completed/errored monitors after a brief delay so tool
83
126
  // consumers have time to read the final state via MonitorList.
84
- setTimeout(() => { this.processes.delete(id); }, 30000);
127
+ setTimeout(() => {
128
+ this.applyReducerEvent({
129
+ type: "MONITOR_PRUNED",
130
+ at: Date.now(),
131
+ source: "system",
132
+ entityType: "monitor",
133
+ entityId: id,
134
+ payload: { id },
135
+ });
136
+ }, 30000);
85
137
  };
86
138
  child.on("close", (code) => {
87
- if (entry.status === "running") {
139
+ if (bp.entry.status === "running") {
88
140
  finish(code, code === 0 ? "completed" : "error");
89
141
  }
90
142
  });
91
143
  child.on("error", (err) => {
92
- if (entry.status === "running") {
93
- entry.status = "error";
94
- entry.completedAt = Date.now();
144
+ if (bp.entry.status === "running") {
145
+ this.applyReducerEvent({
146
+ type: "MONITOR_ERRORED",
147
+ at: Date.now(),
148
+ source: "monitor",
149
+ entityType: "monitor",
150
+ entityId: id,
151
+ payload: {
152
+ id,
153
+ error: err.message,
154
+ },
155
+ });
95
156
  this.pi.events.emit("monitor:error", {
96
157
  monitorId: id,
97
158
  error: err.message,
@@ -104,7 +165,7 @@ export class MonitorManager {
104
165
  });
105
166
  if (timeout > 0) {
106
167
  setTimeout(() => {
107
- if (entry.status === "running") {
168
+ if (bp.entry.status === "running") {
108
169
  this.stop(id);
109
170
  }
110
171
  }, timeout);
@@ -125,7 +186,17 @@ export class MonitorManager {
125
186
  const bp = this.processes.get(id);
126
187
  if (!bp || bp.entry.status !== "running")
127
188
  return false;
128
- bp.entry.status = "stopped";
189
+ this.applyReducerEvent({
190
+ type: "MONITOR_STOPPED",
191
+ at: Date.now(),
192
+ source: "tool",
193
+ entityType: "monitor",
194
+ entityId: id,
195
+ payload: {
196
+ id,
197
+ reason: "manual",
198
+ },
199
+ });
129
200
  bp.proc.kill("SIGTERM");
130
201
  await new Promise((resolve) => {
131
202
  const timer = setTimeout(() => {
@@ -140,7 +211,6 @@ export class MonitorManager {
140
211
  resolve();
141
212
  });
142
213
  });
143
- bp.entry.completedAt = Date.now();
144
214
  bp.completionCallbacks = [];
145
215
  for (const resolve of bp.waiters)
146
216
  resolve();
@@ -157,6 +227,14 @@ export class MonitorManager {
157
227
  }
158
228
  if (bp.entry.status !== "running")
159
229
  return false;
230
+ this.applyReducerEvent({
231
+ type: "MONITOR_ONDONE_REGISTERED",
232
+ at: Date.now(),
233
+ source: "tool",
234
+ entityType: "monitor",
235
+ entityId: id,
236
+ payload: { id },
237
+ });
160
238
  bp.completionCallbacks.push(callback);
161
239
  return true;
162
240
  }
@@ -0,0 +1,82 @@
1
+ import type { MonitorEntry } from "./types.js";
2
+ type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
3
+ export interface MonitorReducerEntry extends MonitorEntry {
4
+ onDoneRegistered?: boolean;
5
+ }
6
+ export interface MonitorReducerState {
7
+ nextId: number;
8
+ monitorsById: Record<string, MonitorReducerEntry>;
9
+ }
10
+ export type MonitorReducerEvent = {
11
+ type: "MONITOR_CREATED";
12
+ at: number;
13
+ source: ReducerSource;
14
+ entityType?: "monitor";
15
+ entityId?: string;
16
+ payload: {
17
+ command: string;
18
+ description?: string;
19
+ timeout: number;
20
+ };
21
+ } | {
22
+ type: "MONITOR_OUTPUT";
23
+ at: number;
24
+ source: ReducerSource;
25
+ entityType?: "monitor";
26
+ entityId?: string;
27
+ payload: {
28
+ id: string;
29
+ line: string;
30
+ };
31
+ } | {
32
+ type: "MONITOR_COMPLETED" | "MONITOR_ERRORED";
33
+ at: number;
34
+ source: ReducerSource;
35
+ entityType?: "monitor";
36
+ entityId?: string;
37
+ payload: {
38
+ id: string;
39
+ exitCode?: number;
40
+ error?: string;
41
+ };
42
+ } | {
43
+ type: "MONITOR_STOPPED";
44
+ at: number;
45
+ source: ReducerSource;
46
+ entityType?: "monitor";
47
+ entityId?: string;
48
+ payload: {
49
+ id: string;
50
+ reason: "manual" | "timeout";
51
+ };
52
+ } | {
53
+ type: "MONITOR_PRUNED" | "MONITOR_ONDONE_REGISTERED";
54
+ at: number;
55
+ source: ReducerSource;
56
+ entityType?: "monitor";
57
+ entityId?: string;
58
+ payload: {
59
+ id: string;
60
+ };
61
+ };
62
+ export type MonitorReducerEffect = {
63
+ type: "PERSIST_MONITOR";
64
+ entityType: "monitor";
65
+ entityId: string;
66
+ payload: {
67
+ monitor: MonitorReducerEntry;
68
+ };
69
+ } | {
70
+ type: "DELETE_MONITOR";
71
+ entityType: "monitor";
72
+ entityId: string;
73
+ payload: {
74
+ id: string;
75
+ };
76
+ };
77
+ export interface MonitorReduceResult {
78
+ state: MonitorReducerState;
79
+ effects: MonitorReducerEffect[];
80
+ }
81
+ export declare function reduceMonitorState(state: MonitorReducerState, event: MonitorReducerEvent): MonitorReduceResult;
82
+ export {};
@@ -0,0 +1,69 @@
1
+ function cloneState(state) {
2
+ return {
3
+ nextId: state.nextId,
4
+ monitorsById: { ...state.monitorsById },
5
+ };
6
+ }
7
+ export function reduceMonitorState(state, event) {
8
+ if (event.type === "MONITOR_CREATED") {
9
+ const next = cloneState(state);
10
+ const id = String(next.nextId++);
11
+ const monitor = {
12
+ id,
13
+ command: event.payload.command,
14
+ description: event.payload.description,
15
+ timeout: event.payload.timeout,
16
+ status: "running",
17
+ startedAt: event.at,
18
+ outputLines: 0,
19
+ outputBuffer: [],
20
+ };
21
+ next.monitorsById[id] = monitor;
22
+ return {
23
+ state: next,
24
+ effects: [{ type: "PERSIST_MONITOR", entityType: "monitor", entityId: id, payload: { monitor } }],
25
+ };
26
+ }
27
+ const id = event.payload.id;
28
+ const current = state.monitorsById[id];
29
+ if (!current)
30
+ return { state, effects: [] };
31
+ if (event.type === "MONITOR_PRUNED") {
32
+ const next = cloneState(state);
33
+ delete next.monitorsById[id];
34
+ return {
35
+ state: next,
36
+ effects: [{ type: "DELETE_MONITOR", entityType: "monitor", entityId: id, payload: { id } }],
37
+ };
38
+ }
39
+ const next = cloneState(state);
40
+ const monitor = { ...current };
41
+ if (event.type === "MONITOR_OUTPUT") {
42
+ monitor.outputLines++;
43
+ if (monitor.outputBuffer.length < 200)
44
+ monitor.outputBuffer = [...monitor.outputBuffer, event.payload.line];
45
+ }
46
+ if (event.type === "MONITOR_COMPLETED") {
47
+ monitor.status = "completed";
48
+ monitor.exitCode = event.payload.exitCode;
49
+ monitor.completedAt = event.at;
50
+ }
51
+ if (event.type === "MONITOR_ERRORED") {
52
+ monitor.status = "error";
53
+ if (event.payload.exitCode !== undefined)
54
+ monitor.exitCode = event.payload.exitCode;
55
+ monitor.completedAt = event.at;
56
+ }
57
+ if (event.type === "MONITOR_STOPPED") {
58
+ monitor.status = "stopped";
59
+ monitor.completedAt = event.at;
60
+ }
61
+ if (event.type === "MONITOR_ONDONE_REGISTERED") {
62
+ monitor.onDoneRegistered = true;
63
+ }
64
+ next.monitorsById[id] = monitor;
65
+ return {
66
+ state: next,
67
+ effects: [{ type: "PERSIST_MONITOR", entityType: "monitor", entityId: id, payload: { monitor } }],
68
+ };
69
+ }
@@ -0,0 +1,81 @@
1
+ type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
2
+ export interface ReducerNotification {
3
+ key: string;
4
+ loopId: string;
5
+ message: string;
6
+ timestamp: number;
7
+ trigger: unknown;
8
+ recurring?: boolean;
9
+ autoTask?: boolean;
10
+ readOnly?: boolean;
11
+ }
12
+ export interface NotificationReducerState {
13
+ notificationsByKey: Record<string, ReducerNotification>;
14
+ agentRunning: boolean;
15
+ hasPendingMessages: boolean;
16
+ }
17
+ export type NotificationReducerEvent = {
18
+ type: "NOTIFICATION_QUEUED";
19
+ at: number;
20
+ source: ReducerSource;
21
+ entityType?: "notification";
22
+ entityId?: string;
23
+ payload: {
24
+ notification: ReducerNotification;
25
+ };
26
+ } | {
27
+ type: "NOTIFICATION_DROPPED";
28
+ at: number;
29
+ source: ReducerSource;
30
+ entityType?: "notification";
31
+ entityId?: string;
32
+ payload: {
33
+ key: string;
34
+ reason: "zero_pending_tasks" | "session_switch" | "session_shutdown" | "superseded";
35
+ };
36
+ } | {
37
+ type: "NOTIFICATION_CLEARED";
38
+ at: number;
39
+ source: ReducerSource;
40
+ entityType?: "notification";
41
+ entityId?: string;
42
+ payload: {
43
+ reason: "session_switch" | "session_shutdown";
44
+ };
45
+ } | {
46
+ type: "NOTIFICATION_FLUSH_REQUESTED";
47
+ at: number;
48
+ source: ReducerSource;
49
+ entityType?: "notification";
50
+ entityId?: string;
51
+ payload: {
52
+ ignorePendingMessages?: boolean;
53
+ };
54
+ } | {
55
+ type: "NOTIFICATION_RUNTIME_UPDATED";
56
+ at: number;
57
+ source: ReducerSource;
58
+ entityType?: "notification";
59
+ entityId?: string;
60
+ payload: {
61
+ agentRunning: boolean;
62
+ hasPendingMessages: boolean;
63
+ };
64
+ };
65
+ export type NotificationReducerEffect = {
66
+ type: "REQUEST_NOTIFICATION_FLUSH";
67
+ payload: Record<string, never>;
68
+ } | {
69
+ type: "DELIVER_NOTIFICATION";
70
+ entityType: "notification";
71
+ entityId: string;
72
+ payload: {
73
+ notification: ReducerNotification;
74
+ };
75
+ };
76
+ export interface NotificationReduceResult {
77
+ state: NotificationReducerState;
78
+ effects: NotificationReducerEffect[];
79
+ }
80
+ export declare function reduceNotificationState(state: NotificationReducerState, event: NotificationReducerEvent): NotificationReduceResult;
81
+ export {};
@@ -0,0 +1,65 @@
1
+ function cloneState(state) {
2
+ return {
3
+ notificationsByKey: { ...state.notificationsByKey },
4
+ agentRunning: state.agentRunning,
5
+ hasPendingMessages: state.hasPendingMessages,
6
+ };
7
+ }
8
+ export function reduceNotificationState(state, event) {
9
+ if (event.type === "NOTIFICATION_QUEUED") {
10
+ const next = cloneState(state);
11
+ next.notificationsByKey[event.payload.notification.key] = event.payload.notification;
12
+ return {
13
+ state: next,
14
+ effects: [{ type: "REQUEST_NOTIFICATION_FLUSH", payload: {} }],
15
+ };
16
+ }
17
+ if (event.type === "NOTIFICATION_DROPPED") {
18
+ const next = cloneState(state);
19
+ delete next.notificationsByKey[event.payload.key];
20
+ return { state: next, effects: [] };
21
+ }
22
+ if (event.type === "NOTIFICATION_CLEARED") {
23
+ return {
24
+ state: {
25
+ ...state,
26
+ notificationsByKey: {},
27
+ },
28
+ effects: [],
29
+ };
30
+ }
31
+ if (event.type === "NOTIFICATION_RUNTIME_UPDATED") {
32
+ return {
33
+ state: {
34
+ ...state,
35
+ agentRunning: event.payload.agentRunning,
36
+ hasPendingMessages: event.payload.hasPendingMessages,
37
+ },
38
+ effects: [],
39
+ };
40
+ }
41
+ if (event.type === "NOTIFICATION_FLUSH_REQUESTED") {
42
+ if (state.agentRunning)
43
+ return { state, effects: [] };
44
+ if (!event.payload.ignorePendingMessages && state.hasPendingMessages) {
45
+ return { state, effects: [] };
46
+ }
47
+ const queued = Object.values(state.notificationsByKey)
48
+ .sort((left, right) => left.timestamp - right.timestamp);
49
+ const nextNotification = queued[0];
50
+ if (!nextNotification)
51
+ return { state, effects: [] };
52
+ const next = cloneState(state);
53
+ delete next.notificationsByKey[nextNotification.key];
54
+ return {
55
+ state: next,
56
+ effects: [{
57
+ type: "DELIVER_NOTIFICATION",
58
+ entityType: "notification",
59
+ entityId: nextNotification.key,
60
+ payload: { notification: nextNotification },
61
+ }],
62
+ };
63
+ }
64
+ return { state, effects: [] };
65
+ }
@@ -0,0 +1,13 @@
1
+ import type { MonitorManager } from "../monitor-manager.js";
2
+ import type { LoopEntry } from "../types.js";
3
+ export interface MonitorOnDoneRuntimeOptions {
4
+ monitorManager: MonitorManager;
5
+ getLoop: (id: string) => LoopEntry | undefined;
6
+ deleteLoop: (id: string) => void;
7
+ onLoopFire: (entry: LoopEntry) => void;
8
+ debug?: (...args: unknown[]) => void;
9
+ }
10
+ export interface MonitorOnDoneRuntime {
11
+ register(doneLoop: LoopEntry, monitorId: string): void;
12
+ }
13
+ export declare function createMonitorOnDoneRuntime(options: MonitorOnDoneRuntimeOptions): MonitorOnDoneRuntime;