@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.
- package/README.md +11 -1
- package/dist/coordinator.d.ts +35 -0
- package/dist/coordinator.js +56 -0
- package/dist/goal-types.d.ts +78 -0
- package/dist/goal-types.js +1 -0
- package/dist/goal-verifier.d.ts +20 -0
- package/dist/goal-verifier.js +198 -0
- package/dist/index.js +182 -40
- package/dist/loop-reducer.d.ts +63 -0
- package/dist/loop-reducer.js +67 -0
- package/dist/monitor-completion-coordinator.d.ts +10 -0
- package/dist/monitor-completion-coordinator.js +13 -0
- package/dist/monitor-manager.d.ts +2 -0
- package/dist/monitor-manager.js +107 -29
- package/dist/monitor-reducer.d.ts +82 -0
- package/dist/monitor-reducer.js +69 -0
- package/dist/notification-reducer.d.ts +81 -0
- package/dist/notification-reducer.js +65 -0
- package/dist/store.d.ts +2 -0
- package/dist/store.js +118 -44
- package/dist/task-backlog-coordinator.d.ts +12 -0
- package/dist/task-backlog-coordinator.js +22 -0
- package/dist/task-reducer.d.ts +66 -0
- package/dist/task-reducer.js +76 -0
- package/dist/task-store.d.ts +2 -0
- package/dist/task-store.js +82 -30
- package/docs/architecture/goal-state-schema.md +505 -0
- package/docs/architecture/state-machine-migration.md +546 -0
- package/docs/architecture/state-machine-reducer-event-model.md +823 -0
- package/docs/architecture/state-machine-test-matrix.md +249 -0
- package/docs/architecture/state-machine-transition-map.md +436 -0
- package/package.json +1 -1
- package/src/coordinator.ts +115 -0
- package/src/goal-types.ts +99 -0
- package/src/goal-verifier.ts +241 -0
- package/src/index.ts +209 -39
- package/src/loop-reducer.ts +148 -0
- package/src/monitor-completion-coordinator.ts +24 -0
- package/src/monitor-manager.ts +115 -27
- package/src/monitor-reducer.ts +166 -0
- package/src/notification-reducer.ts +155 -0
- package/src/store.ts +119 -43
- package/src/task-backlog-coordinator.ts +32 -0
- package/src/task-reducer.ts +152 -0
- package/src/task-store.ts +84 -27
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
# pi-loop Reducer Event Model
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
This document defines the reducer-facing event model for the planned state-machine extraction.
|
|
6
|
+
|
|
7
|
+
It sits between:
|
|
8
|
+
|
|
9
|
+
- `docs/architecture/state-machine-transition-map.md`
|
|
10
|
+
- `docs/architecture/state-machine-test-matrix.md`
|
|
11
|
+
|
|
12
|
+
Its job is to give tasks `#6` and `#7` a stable contract for:
|
|
13
|
+
|
|
14
|
+
- event names
|
|
15
|
+
- reducer inputs
|
|
16
|
+
- reducer outputs
|
|
17
|
+
- side-effect descriptions
|
|
18
|
+
- entity ownership boundaries
|
|
19
|
+
|
|
20
|
+
This is a design artifact only. It does **not** change runtime behavior yet.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 1. Design goals
|
|
25
|
+
|
|
26
|
+
The event model should:
|
|
27
|
+
|
|
28
|
+
1. preserve current semantics before simplifying implementation
|
|
29
|
+
2. keep reducers pure
|
|
30
|
+
3. separate **state transitions** from **effects**
|
|
31
|
+
4. avoid one giant FSM for everything
|
|
32
|
+
5. make cross-entity coordination explicit
|
|
33
|
+
6. support future Goal orchestration without rewriting the core model again
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. Model shape
|
|
38
|
+
|
|
39
|
+
Use **separate reducers per entity family** plus a thin coordinator.
|
|
40
|
+
|
|
41
|
+
### Reducers
|
|
42
|
+
|
|
43
|
+
- `reduceTaskState(...)`
|
|
44
|
+
- `reduceLoopState(...)`
|
|
45
|
+
- `reduceMonitorState(...)`
|
|
46
|
+
- `reduceNotificationState(...)`
|
|
47
|
+
- later: `reduceGoalState(...)`
|
|
48
|
+
|
|
49
|
+
### Coordinator
|
|
50
|
+
|
|
51
|
+
A small coordinator should:
|
|
52
|
+
|
|
53
|
+
- receive runtime events
|
|
54
|
+
- fan them out to the relevant reducer(s)
|
|
55
|
+
- collect emitted effects
|
|
56
|
+
- execute those effects in order
|
|
57
|
+
- emit any derived follow-up events
|
|
58
|
+
|
|
59
|
+
This keeps:
|
|
60
|
+
|
|
61
|
+
- reducers deterministic
|
|
62
|
+
- side effects testable as data
|
|
63
|
+
- cross-entity logic explicit instead of buried in `src/index.ts`
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 3. Shared event envelope
|
|
68
|
+
|
|
69
|
+
All reducer events should use one envelope shape.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
interface ReducerEvent<TType extends string = string, TPayload = unknown> {
|
|
73
|
+
type: TType;
|
|
74
|
+
at: number;
|
|
75
|
+
source:
|
|
76
|
+
| "tool"
|
|
77
|
+
| "command"
|
|
78
|
+
| "scheduler"
|
|
79
|
+
| "eventbus"
|
|
80
|
+
| "monitor"
|
|
81
|
+
| "session"
|
|
82
|
+
| "coordinator"
|
|
83
|
+
| "system";
|
|
84
|
+
entityType?: "task" | "loop" | "monitor" | "notification" | "goal";
|
|
85
|
+
entityId?: string;
|
|
86
|
+
payload: TPayload;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Rules
|
|
91
|
+
|
|
92
|
+
- `type` is the stable contract
|
|
93
|
+
- `at` is required for ordering and deterministic tests
|
|
94
|
+
- `source` records where the event originated
|
|
95
|
+
- `entityType` and `entityId` are optional because some events are global
|
|
96
|
+
- `payload` must be serializable test data
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 4. Shared effect envelope
|
|
101
|
+
|
|
102
|
+
Reducers do not call Pi APIs directly. They emit effects.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
interface ReducerEffect<TEffect extends string = string, TPayload = unknown> {
|
|
106
|
+
type: TEffect;
|
|
107
|
+
entityType?: "task" | "loop" | "monitor" | "notification" | "goal";
|
|
108
|
+
entityId?: string;
|
|
109
|
+
payload: TPayload;
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Effect classes
|
|
114
|
+
|
|
115
|
+
1. **persistence effects**
|
|
116
|
+
- save/delete/update store entries
|
|
117
|
+
2. **runtime wiring effects**
|
|
118
|
+
- add/remove scheduler entries
|
|
119
|
+
- add/remove event subscriptions
|
|
120
|
+
3. **wake/message effects**
|
|
121
|
+
- queue notification
|
|
122
|
+
- flush notification
|
|
123
|
+
- clear notifications
|
|
124
|
+
4. **cleanup effects**
|
|
125
|
+
- prune tasks
|
|
126
|
+
- prune monitors
|
|
127
|
+
- delete stale loops
|
|
128
|
+
5. **derived-event effects**
|
|
129
|
+
- request that the coordinator dispatch another reducer event
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 5. State ownership boundaries
|
|
134
|
+
|
|
135
|
+
### Task reducer owns
|
|
136
|
+
|
|
137
|
+
- task entities
|
|
138
|
+
- task status transitions
|
|
139
|
+
- monotonic task ids
|
|
140
|
+
- completed-task retention vs prune state
|
|
141
|
+
|
|
142
|
+
### Loop reducer owns
|
|
143
|
+
|
|
144
|
+
- loop entities
|
|
145
|
+
- active/paused/terminal lifecycle
|
|
146
|
+
- fire counts
|
|
147
|
+
- maxFires/expiry decisions
|
|
148
|
+
- backlog-loop classification
|
|
149
|
+
|
|
150
|
+
### Monitor reducer owns
|
|
151
|
+
|
|
152
|
+
- monitor entities
|
|
153
|
+
- running/completed/error/stopped lifecycle
|
|
154
|
+
- terminal retention/prune status
|
|
155
|
+
- completion callback registration state
|
|
156
|
+
|
|
157
|
+
### Notification reducer owns
|
|
158
|
+
|
|
159
|
+
- queued notifications
|
|
160
|
+
- dedupe behavior
|
|
161
|
+
- flush eligibility state
|
|
162
|
+
- cleared/dropped/delivered outcomes
|
|
163
|
+
|
|
164
|
+
### Coordinator owns
|
|
165
|
+
|
|
166
|
+
- event ordering across reducers
|
|
167
|
+
- derived-event dispatch
|
|
168
|
+
- effect execution
|
|
169
|
+
- fan-out rules like "monitor completed -> queue wake"
|
|
170
|
+
- runtime API integration with Pi/tool layer
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 6. Event vocabulary
|
|
175
|
+
|
|
176
|
+
The event set should be small but explicit.
|
|
177
|
+
|
|
178
|
+
### 6.1 Task events
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
type TaskEventType =
|
|
182
|
+
| "TASK_CREATED"
|
|
183
|
+
| "TASK_STARTED"
|
|
184
|
+
| "TASK_COMPLETED"
|
|
185
|
+
| "TASK_REOPENED"
|
|
186
|
+
| "TASK_UPDATED"
|
|
187
|
+
| "TASK_DELETED"
|
|
188
|
+
| "TASKS_PRUNED";
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Semantics
|
|
192
|
+
|
|
193
|
+
- `TASK_CREATED`: create a new pending task
|
|
194
|
+
- `TASK_STARTED`: transition pending -> in_progress
|
|
195
|
+
- `TASK_COMPLETED`: transition pending|in_progress -> completed
|
|
196
|
+
- `TASK_REOPENED`: transition completed -> pending
|
|
197
|
+
- `TASK_UPDATED`: subject/description-only edits or generic fallback
|
|
198
|
+
- `TASK_DELETED`: remove one task
|
|
199
|
+
- `TASKS_PRUNED`: remove all completed tasks by policy trigger
|
|
200
|
+
|
|
201
|
+
### Recommended payloads
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
type TaskCreatedPayload = {
|
|
205
|
+
id: string;
|
|
206
|
+
subject: string;
|
|
207
|
+
description: string;
|
|
208
|
+
metadata?: Record<string, unknown>;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
type TaskStatePayload = { id: string };
|
|
212
|
+
|
|
213
|
+
type TaskUpdatedPayload = {
|
|
214
|
+
id: string;
|
|
215
|
+
subject?: string;
|
|
216
|
+
description?: string;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
type TasksPrunedPayload = {
|
|
220
|
+
reason: "git_commit" | "zero_pending_cleanup" | "manual";
|
|
221
|
+
};
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
### 6.2 Loop events
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
type LoopEventType =
|
|
230
|
+
| "LOOP_CREATED"
|
|
231
|
+
| "LOOP_PAUSED"
|
|
232
|
+
| "LOOP_RESUMED"
|
|
233
|
+
| "LOOP_DELETED"
|
|
234
|
+
| "LOOP_FIRED"
|
|
235
|
+
| "LOOP_MAX_FIRES_REACHED"
|
|
236
|
+
| "LOOP_EXPIRED"
|
|
237
|
+
| "LOOP_BACKLOG_EMPTY"
|
|
238
|
+
| "LOOP_BOOTSTRAP_REQUESTED"
|
|
239
|
+
| "LOOP_ARMING_RECONCILE_REQUESTED";
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Semantics
|
|
243
|
+
|
|
244
|
+
- `LOOP_CREATED`: a new loop exists in active state
|
|
245
|
+
- `LOOP_PAUSED`: active -> paused
|
|
246
|
+
- `LOOP_RESUMED`: paused -> active
|
|
247
|
+
- `LOOP_DELETED`: terminal removal by user/system
|
|
248
|
+
- `LOOP_FIRED`: one due fire occurred
|
|
249
|
+
- `LOOP_MAX_FIRES_REACHED`: final fire consumed limit
|
|
250
|
+
- `LOOP_EXPIRED`: lifetime cap or stale-session expiry
|
|
251
|
+
- `LOOP_BACKLOG_EMPTY`: taskBacklog loop should self-delete
|
|
252
|
+
- `LOOP_BOOTSTRAP_REQUESTED`: existing backlog should create initial wake
|
|
253
|
+
- `LOOP_ARMING_RECONCILE_REQUESTED`: sync loop state with scheduler/subscriptions
|
|
254
|
+
|
|
255
|
+
### Recommended payloads
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
type LoopCreatedPayload = {
|
|
259
|
+
id: string;
|
|
260
|
+
prompt: string;
|
|
261
|
+
trigger: unknown;
|
|
262
|
+
recurring: boolean;
|
|
263
|
+
autoTask?: boolean;
|
|
264
|
+
taskBacklog?: boolean;
|
|
265
|
+
readOnly?: boolean;
|
|
266
|
+
maxFires?: number;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
type LoopIdPayload = { id: string };
|
|
270
|
+
|
|
271
|
+
type LoopFiredPayload = {
|
|
272
|
+
id: string;
|
|
273
|
+
trigger: unknown;
|
|
274
|
+
recurring: boolean;
|
|
275
|
+
autoTask?: boolean;
|
|
276
|
+
readOnly?: boolean;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
type LoopExpiredPayload = {
|
|
280
|
+
id: string;
|
|
281
|
+
reason: "expires_at" | "resume_event_stale" | "already_completed_monitor";
|
|
282
|
+
};
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
### 6.3 Monitor events
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
type MonitorEventType =
|
|
291
|
+
| "MONITOR_CREATED"
|
|
292
|
+
| "MONITOR_OUTPUT"
|
|
293
|
+
| "MONITOR_COMPLETED"
|
|
294
|
+
| "MONITOR_ERRORED"
|
|
295
|
+
| "MONITOR_STOPPED"
|
|
296
|
+
| "MONITOR_PRUNED"
|
|
297
|
+
| "MONITOR_ONDONE_REGISTERED";
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Semantics
|
|
301
|
+
|
|
302
|
+
- `MONITOR_CREATED`: running monitor started
|
|
303
|
+
- `MONITOR_OUTPUT`: output line buffered/emitted
|
|
304
|
+
- `MONITOR_COMPLETED`: clean exit
|
|
305
|
+
- `MONITOR_ERRORED`: process error or nonzero exit
|
|
306
|
+
- `MONITOR_STOPPED`: explicit stop path
|
|
307
|
+
- `MONITOR_PRUNED`: retention timeout elapsed
|
|
308
|
+
- `MONITOR_ONDONE_REGISTERED`: completion callback attached
|
|
309
|
+
|
|
310
|
+
### Recommended payloads
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
type MonitorCreatedPayload = {
|
|
314
|
+
id: string;
|
|
315
|
+
command: string;
|
|
316
|
+
description?: string;
|
|
317
|
+
timeout: number;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
type MonitorOutputPayload = {
|
|
321
|
+
id: string;
|
|
322
|
+
line: string;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
type MonitorCompletedPayload = {
|
|
326
|
+
id: string;
|
|
327
|
+
exitCode?: number;
|
|
328
|
+
outputLines: number;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
type MonitorErroredPayload = {
|
|
332
|
+
id: string;
|
|
333
|
+
exitCode?: number;
|
|
334
|
+
error?: string;
|
|
335
|
+
outputLines?: number;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
type MonitorStoppedPayload = {
|
|
339
|
+
id: string;
|
|
340
|
+
reason: "manual" | "timeout";
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
type MonitorOnDoneRegisteredPayload = {
|
|
344
|
+
id: string;
|
|
345
|
+
prompt: string;
|
|
346
|
+
loopId?: string;
|
|
347
|
+
};
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
### 6.4 Notification events
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
type NotificationEventType =
|
|
356
|
+
| "NOTIFICATION_QUEUED"
|
|
357
|
+
| "NOTIFICATION_REPLACED"
|
|
358
|
+
| "NOTIFICATION_DELIVERED"
|
|
359
|
+
| "NOTIFICATION_DROPPED"
|
|
360
|
+
| "NOTIFICATION_CLEARED"
|
|
361
|
+
| "NOTIFICATION_FLUSH_REQUESTED";
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Semantics
|
|
365
|
+
|
|
366
|
+
- `NOTIFICATION_QUEUED`: add pending wake
|
|
367
|
+
- `NOTIFICATION_REPLACED`: recurring dedupe replaced earlier wake
|
|
368
|
+
- `NOTIFICATION_DELIVERED`: sent to Pi successfully
|
|
369
|
+
- `NOTIFICATION_DROPPED`: removed without delivery by policy
|
|
370
|
+
- `NOTIFICATION_CLEARED`: removed due to session change/shutdown
|
|
371
|
+
- `NOTIFICATION_FLUSH_REQUESTED`: coordinator should attempt delivery pass
|
|
372
|
+
|
|
373
|
+
### Recommended payloads
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
type NotificationQueuedPayload = {
|
|
377
|
+
key: string;
|
|
378
|
+
loopId: string;
|
|
379
|
+
message: string;
|
|
380
|
+
recurring?: boolean;
|
|
381
|
+
autoTask?: boolean;
|
|
382
|
+
readOnly?: boolean;
|
|
383
|
+
trigger: unknown;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
type NotificationDroppedPayload = {
|
|
387
|
+
key: string;
|
|
388
|
+
reason:
|
|
389
|
+
| "zero_pending_tasks"
|
|
390
|
+
| "session_switch"
|
|
391
|
+
| "session_shutdown"
|
|
392
|
+
| "superseded";
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
type NotificationClearedPayload = {
|
|
396
|
+
reason: "session_switch" | "session_shutdown";
|
|
397
|
+
};
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
### 6.5 Session / coordinator events
|
|
403
|
+
|
|
404
|
+
These are not owned by one entity reducer, but they drive coordination.
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
type CoordinatorEventType =
|
|
408
|
+
| "SESSION_TURN_STARTED"
|
|
409
|
+
| "SESSION_BEFORE_AGENT_START"
|
|
410
|
+
| "SESSION_AGENT_STARTED"
|
|
411
|
+
| "SESSION_AGENT_ENDED"
|
|
412
|
+
| "SESSION_SWITCHED"
|
|
413
|
+
| "SESSION_SHUTDOWN"
|
|
414
|
+
| "TASK_BACKLOG_THRESHOLD_REACHED"
|
|
415
|
+
| "TASK_BACKLOG_EMPTIED"
|
|
416
|
+
| "TASK_PROVIDER_DETECTED"
|
|
417
|
+
| "TASK_PROVIDER_UNAVAILABLE"
|
|
418
|
+
| "GIT_COMMIT_SUCCEEDED";
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Notes
|
|
422
|
+
|
|
423
|
+
- session events should usually emit **derived entity events** rather than mutate entity state directly
|
|
424
|
+
- `GIT_COMMIT_SUCCEEDED` should derive `TASKS_PRUNED`
|
|
425
|
+
- `TASK_BACKLOG_THRESHOLD_REACHED` should derive `LOOP_CREATED` for the worker loop if absent
|
|
426
|
+
- `TASK_BACKLOG_EMPTIED` should derive `LOOP_BACKLOG_EMPTY`
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## 7. Reducer outputs
|
|
431
|
+
|
|
432
|
+
Each reducer should return both next state and effects.
|
|
433
|
+
|
|
434
|
+
```ts
|
|
435
|
+
interface ReduceResult<TState> {
|
|
436
|
+
state: TState;
|
|
437
|
+
effects: ReducerEffect[];
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Rule
|
|
442
|
+
|
|
443
|
+
A reducer may emit effects, but it may **not**:
|
|
444
|
+
|
|
445
|
+
- call Pi APIs
|
|
446
|
+
- read system time directly
|
|
447
|
+
- mutate unrelated reducer state
|
|
448
|
+
- inspect external runtime maps directly
|
|
449
|
+
|
|
450
|
+
All external facts must arrive as events.
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## 8. Effect vocabulary
|
|
455
|
+
|
|
456
|
+
Use a small, explicit effect set.
|
|
457
|
+
|
|
458
|
+
### 8.1 Persistence effects
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
type PersistenceEffectType =
|
|
462
|
+
| "PERSIST_TASK"
|
|
463
|
+
| "DELETE_TASK"
|
|
464
|
+
| "PERSIST_LOOP"
|
|
465
|
+
| "DELETE_LOOP"
|
|
466
|
+
| "PERSIST_MONITOR"
|
|
467
|
+
| "DELETE_MONITOR";
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### 8.2 Runtime wiring effects
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
type RuntimeEffectType =
|
|
474
|
+
| "ARM_LOOP_RUNTIME"
|
|
475
|
+
| "DISARM_LOOP_RUNTIME"
|
|
476
|
+
| "REGISTER_MONITOR_ONDONE"
|
|
477
|
+
| "SCHEDULE_MONITOR_PRUNE";
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### 8.3 Notification effects
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
type NotificationEffectType =
|
|
484
|
+
| "QUEUE_NOTIFICATION"
|
|
485
|
+
| "DELIVER_NOTIFICATION"
|
|
486
|
+
| "CLEAR_NOTIFICATIONS"
|
|
487
|
+
| "REQUEST_NOTIFICATION_FLUSH";
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### 8.4 Derived-event effects
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
type DerivedEventEffectType = "DISPATCH_EVENT";
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### 8.5 Cleanup effects
|
|
497
|
+
|
|
498
|
+
```ts
|
|
499
|
+
type CleanupEffectType =
|
|
500
|
+
| "PRUNE_COMPLETED_TASKS"
|
|
501
|
+
| "PRUNE_TERMINAL_MONITORS"
|
|
502
|
+
| "RECONCILE_BACKLOG_LOOPS";
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
## 9. Event-to-current-runtime mapping
|
|
508
|
+
|
|
509
|
+
This section maps today's code paths to the future event model.
|
|
510
|
+
|
|
511
|
+
### Current: `TaskCreate`
|
|
512
|
+
|
|
513
|
+
Emit:
|
|
514
|
+
|
|
515
|
+
- `TASK_CREATED`
|
|
516
|
+
- maybe `TASK_BACKLOG_THRESHOLD_REACHED`
|
|
517
|
+
|
|
518
|
+
### Current: `TaskUpdate(status=in_progress)`
|
|
519
|
+
|
|
520
|
+
Emit:
|
|
521
|
+
|
|
522
|
+
- `TASK_STARTED`
|
|
523
|
+
|
|
524
|
+
### Current: `TaskUpdate(status=completed)`
|
|
525
|
+
|
|
526
|
+
Emit:
|
|
527
|
+
|
|
528
|
+
- `TASK_COMPLETED`
|
|
529
|
+
- maybe `TASK_BACKLOG_EMPTIED`
|
|
530
|
+
|
|
531
|
+
### Current: `TaskUpdate(status=pending)` on completed task
|
|
532
|
+
|
|
533
|
+
Emit:
|
|
534
|
+
|
|
535
|
+
- `TASK_REOPENED`
|
|
536
|
+
|
|
537
|
+
### Current: `LoopCreate`
|
|
538
|
+
|
|
539
|
+
Emit:
|
|
540
|
+
|
|
541
|
+
- `LOOP_CREATED`
|
|
542
|
+
- maybe `LOOP_BOOTSTRAP_REQUESTED`
|
|
543
|
+
- `LOOP_ARMING_RECONCILE_REQUESTED`
|
|
544
|
+
|
|
545
|
+
### Current: `LoopDelete(action=pause)`
|
|
546
|
+
|
|
547
|
+
Emit:
|
|
548
|
+
|
|
549
|
+
- `LOOP_PAUSED`
|
|
550
|
+
|
|
551
|
+
### Current: resume action from interactive UI
|
|
552
|
+
|
|
553
|
+
Emit:
|
|
554
|
+
|
|
555
|
+
- `LOOP_RESUMED`
|
|
556
|
+
- `LOOP_ARMING_RECONCILE_REQUESTED`
|
|
557
|
+
|
|
558
|
+
### Current: scheduler/event trigger fire
|
|
559
|
+
|
|
560
|
+
Emit:
|
|
561
|
+
|
|
562
|
+
- `LOOP_FIRED`
|
|
563
|
+
- maybe `LOOP_MAX_FIRES_REACHED`
|
|
564
|
+
- derived `NOTIFICATION_QUEUED`
|
|
565
|
+
|
|
566
|
+
### Current: monitor start
|
|
567
|
+
|
|
568
|
+
Emit:
|
|
569
|
+
|
|
570
|
+
- `MONITOR_CREATED`
|
|
571
|
+
- maybe `MONITOR_ONDONE_REGISTERED`
|
|
572
|
+
|
|
573
|
+
### Current: monitor clean exit
|
|
574
|
+
|
|
575
|
+
Emit:
|
|
576
|
+
|
|
577
|
+
- `MONITOR_COMPLETED`
|
|
578
|
+
- derived `NOTIFICATION_QUEUED` for onDone path
|
|
579
|
+
- derived `MONITOR_PRUNED` after retention timeout
|
|
580
|
+
|
|
581
|
+
### Current: monitor error path
|
|
582
|
+
|
|
583
|
+
Emit:
|
|
584
|
+
|
|
585
|
+
- `MONITOR_ERRORED`
|
|
586
|
+
|
|
587
|
+
### Current: monitor stop
|
|
588
|
+
|
|
589
|
+
Emit:
|
|
590
|
+
|
|
591
|
+
- `MONITOR_STOPPED`
|
|
592
|
+
|
|
593
|
+
### Current: `session_switch`
|
|
594
|
+
|
|
595
|
+
Emit:
|
|
596
|
+
|
|
597
|
+
- `SESSION_SWITCHED`
|
|
598
|
+
- derived `NOTIFICATION_CLEARED`
|
|
599
|
+
- maybe derived `LOOP_EXPIRED` for stale event/hybrid loops
|
|
600
|
+
- maybe derived `LOOP_DELETED` for memory-scope non-resume clearing
|
|
601
|
+
|
|
602
|
+
### Current: `session_shutdown`
|
|
603
|
+
|
|
604
|
+
Emit:
|
|
605
|
+
|
|
606
|
+
- `SESSION_SHUTDOWN`
|
|
607
|
+
- derived `NOTIFICATION_CLEARED`
|
|
608
|
+
|
|
609
|
+
### Current: successful `git commit`
|
|
610
|
+
|
|
611
|
+
Emit:
|
|
612
|
+
|
|
613
|
+
- `GIT_COMMIT_SUCCEEDED`
|
|
614
|
+
- derived `TASKS_PRUNED`
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
## 10. Coordinator rules
|
|
619
|
+
|
|
620
|
+
These rules should move out of ad hoc control flow into explicit coordinator logic.
|
|
621
|
+
|
|
622
|
+
### Rule A — backlog threshold auto-worker creation
|
|
623
|
+
|
|
624
|
+
When native pending tasks cross the threshold:
|
|
625
|
+
|
|
626
|
+
1. dispatch `TASK_BACKLOG_THRESHOLD_REACHED`
|
|
627
|
+
2. if no worker loop exists, emit `LOOP_CREATED`
|
|
628
|
+
3. emit `LOOP_BOOTSTRAP_REQUESTED` if backlog already exists
|
|
629
|
+
|
|
630
|
+
### Rule B — backlog empties
|
|
631
|
+
|
|
632
|
+
When pending tasks reach zero:
|
|
633
|
+
|
|
634
|
+
1. dispatch `TASK_BACKLOG_EMPTIED`
|
|
635
|
+
2. derive `LOOP_BACKLOG_EMPTY` for each `taskBacklog` loop
|
|
636
|
+
3. reducer emits `DELETE_LOOP`
|
|
637
|
+
|
|
638
|
+
### Rule C — loop fire -> notification
|
|
639
|
+
|
|
640
|
+
When `LOOP_FIRED` occurs:
|
|
641
|
+
|
|
642
|
+
1. loop reducer increments `fireCount`
|
|
643
|
+
2. if limit reached, also derive `LOOP_MAX_FIRES_REACHED`
|
|
644
|
+
3. coordinator requests notification queueing
|
|
645
|
+
4. notification reducer decides replace vs append
|
|
646
|
+
|
|
647
|
+
### Rule D — notification flush
|
|
648
|
+
|
|
649
|
+
When idle/session conditions allow:
|
|
650
|
+
|
|
651
|
+
1. dispatch `NOTIFICATION_FLUSH_REQUESTED`
|
|
652
|
+
2. notification reducer chooses next deliverable item
|
|
653
|
+
3. emit `DELIVER_NOTIFICATION`
|
|
654
|
+
4. if delivery blocked, keep state unchanged
|
|
655
|
+
|
|
656
|
+
### Rule E — monitor onDone
|
|
657
|
+
|
|
658
|
+
When a monitor with onDone completes:
|
|
659
|
+
|
|
660
|
+
1. dispatch `MONITOR_COMPLETED`
|
|
661
|
+
2. derive `NOTIFICATION_QUEUED` directly
|
|
662
|
+
3. do **not** depend on generic eventbus delivery semantics for correctness
|
|
663
|
+
|
|
664
|
+
### Rule F — session switch/shutdown
|
|
665
|
+
|
|
666
|
+
1. dispatch session event
|
|
667
|
+
2. clear pending notifications
|
|
668
|
+
3. derive any loop cleanup events required by scope/resume rules
|
|
669
|
+
4. reconcile runtime wiring afterward
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## 11. Reducer state shapes
|
|
674
|
+
|
|
675
|
+
These should stay small and explicit.
|
|
676
|
+
|
|
677
|
+
### Task reducer state
|
|
678
|
+
|
|
679
|
+
```ts
|
|
680
|
+
interface TaskReducerState {
|
|
681
|
+
tasksById: Record<string, TaskEntryView>;
|
|
682
|
+
nextId: number;
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### Loop reducer state
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
interface LoopReducerState {
|
|
690
|
+
loopsById: Record<string, LoopEntryView>;
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### Monitor reducer state
|
|
695
|
+
|
|
696
|
+
```ts
|
|
697
|
+
interface MonitorReducerState {
|
|
698
|
+
monitorsById: Record<string, MonitorEntryView>;
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
### Notification reducer state
|
|
703
|
+
|
|
704
|
+
```ts
|
|
705
|
+
interface NotificationReducerState {
|
|
706
|
+
notificationsByKey: Record<string, PendingNotificationView>;
|
|
707
|
+
agentRunning: boolean;
|
|
708
|
+
hasPendingMessages: boolean;
|
|
709
|
+
}
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### Important
|
|
713
|
+
|
|
714
|
+
Runtime-only objects like:
|
|
715
|
+
|
|
716
|
+
- child process handles
|
|
717
|
+
- scheduler timer internals
|
|
718
|
+
- event unsubscribe functions
|
|
719
|
+
- abort controllers
|
|
720
|
+
|
|
721
|
+
should remain **outside** reducer state. Reducers track only their logical status.
|
|
722
|
+
|
|
723
|
+
---
|
|
724
|
+
|
|
725
|
+
## 12. Recommended migration order
|
|
726
|
+
|
|
727
|
+
### Step 1 — task reducer
|
|
728
|
+
|
|
729
|
+
Extract first because it is the simplest and already well covered.
|
|
730
|
+
|
|
731
|
+
Start with:
|
|
732
|
+
|
|
733
|
+
- `TASK_CREATED`
|
|
734
|
+
- `TASK_STARTED`
|
|
735
|
+
- `TASK_COMPLETED`
|
|
736
|
+
- `TASK_REOPENED`
|
|
737
|
+
- `TASK_DELETED`
|
|
738
|
+
- `TASKS_PRUNED`
|
|
739
|
+
|
|
740
|
+
### Step 2 — loop reducer
|
|
741
|
+
|
|
742
|
+
Extract next, but keep runtime arming/disarming as effects.
|
|
743
|
+
|
|
744
|
+
Start with:
|
|
745
|
+
|
|
746
|
+
- `LOOP_CREATED`
|
|
747
|
+
- `LOOP_PAUSED`
|
|
748
|
+
- `LOOP_RESUMED`
|
|
749
|
+
- `LOOP_DELETED`
|
|
750
|
+
- `LOOP_FIRED`
|
|
751
|
+
- `LOOP_MAX_FIRES_REACHED`
|
|
752
|
+
- `LOOP_EXPIRED`
|
|
753
|
+
- `LOOP_BACKLOG_EMPTY`
|
|
754
|
+
|
|
755
|
+
### Step 3 — notification reducer
|
|
756
|
+
|
|
757
|
+
Then extract queue/dedupe/flush rules.
|
|
758
|
+
|
|
759
|
+
Start with:
|
|
760
|
+
|
|
761
|
+
- `NOTIFICATION_QUEUED`
|
|
762
|
+
- `NOTIFICATION_REPLACED`
|
|
763
|
+
- `NOTIFICATION_DROPPED`
|
|
764
|
+
- `NOTIFICATION_CLEARED`
|
|
765
|
+
- `NOTIFICATION_FLUSH_REQUESTED`
|
|
766
|
+
|
|
767
|
+
### Step 4 — monitor reducer
|
|
768
|
+
|
|
769
|
+
Finally extract monitor lifecycle because it has the most runtime coupling.
|
|
770
|
+
|
|
771
|
+
Start with:
|
|
772
|
+
|
|
773
|
+
- `MONITOR_CREATED`
|
|
774
|
+
- `MONITOR_COMPLETED`
|
|
775
|
+
- `MONITOR_ERRORED`
|
|
776
|
+
- `MONITOR_STOPPED`
|
|
777
|
+
- `MONITOR_PRUNED`
|
|
778
|
+
- `MONITOR_ONDONE_REGISTERED`
|
|
779
|
+
|
|
780
|
+
---
|
|
781
|
+
|
|
782
|
+
## 13. Test alignment
|
|
783
|
+
|
|
784
|
+
Every event in this model should map cleanly to the existing matrix.
|
|
785
|
+
|
|
786
|
+
### Examples
|
|
787
|
+
|
|
788
|
+
- matrix `T-03` -> `TASK_STARTED`
|
|
789
|
+
- matrix `T-06` -> `TASK_REOPENED`
|
|
790
|
+
- matrix `L-08` -> `LOOP_FIRED` then `LOOP_DELETED`
|
|
791
|
+
- matrix `M-09` -> `MONITOR_COMPLETED` then derived `NOTIFICATION_QUEUED`
|
|
792
|
+
- matrix `N-08` -> `SESSION_SWITCHED` then derived `NOTIFICATION_CLEARED`
|
|
793
|
+
- matrix `S-08` -> `MONITOR_COMPLETED` while `agentRunning=true`, then `SESSION_AGENT_ENDED`, then `NOTIFICATION_DELIVERED`
|
|
794
|
+
|
|
795
|
+
If an event cannot be tied to a transition/invariant/scenario row, it is probably too vague or too implementation-specific.
|
|
796
|
+
|
|
797
|
+
---
|
|
798
|
+
|
|
799
|
+
## 14. Non-goals
|
|
800
|
+
|
|
801
|
+
This event model does **not** yet define:
|
|
802
|
+
|
|
803
|
+
- Goal reducer fields
|
|
804
|
+
- persistent serialization format for goals
|
|
805
|
+
- UI rendering model
|
|
806
|
+
- task provider abstraction replacement
|
|
807
|
+
- a generalized event sourcing log
|
|
808
|
+
|
|
809
|
+
It only defines the reducer contract needed to extract the current runtime safely.
|
|
810
|
+
|
|
811
|
+
---
|
|
812
|
+
|
|
813
|
+
## 15. Summary
|
|
814
|
+
|
|
815
|
+
The recommended architecture is:
|
|
816
|
+
|
|
817
|
+
- separate reducers per entity family
|
|
818
|
+
- one shared event envelope
|
|
819
|
+
- one shared effect envelope
|
|
820
|
+
- thin coordinator for cross-entity rules
|
|
821
|
+
- runtime integration only in effect execution
|
|
822
|
+
|
|
823
|
+
That gives tasks `#6` and `#7` a concrete target while preserving the current semantics locked down by the new tests.
|