@trevonistrevon/pi-loop 0.5.8 → 0.6.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/.release-please-manifest.json +3 -0
- package/AGENTS.md +52 -12
- package/CHANGELOG.md +59 -0
- package/CONTRIBUTING.md +13 -3
- package/Dockerfile +17 -0
- package/README.md +47 -0
- package/coverage/coverage-final.json +35 -31
- package/coverage/index.html +64 -49
- package/coverage/src/api.ts.html +193 -0
- package/coverage/src/commands/index.html +32 -32
- package/coverage/src/commands/loop-command.ts.html +170 -170
- package/coverage/src/commands/tasks-command.ts.html +135 -135
- package/coverage/src/coordinator.ts.html +26 -26
- package/coverage/src/index.html +61 -106
- package/coverage/src/index.ts.html +280 -94
- package/coverage/src/loop-parse.ts.html +90 -57
- package/coverage/src/loop-reducer.ts.html +25 -25
- package/coverage/src/monitor-completion-coordinator.ts.html +1 -1
- package/coverage/src/monitor-manager.ts.html +95 -71
- package/coverage/src/monitor-reducer.ts.html +15 -15
- package/coverage/src/notification-reducer.ts.html +7 -7
- package/coverage/src/reducer-backed-store.ts.html +57 -57
- package/coverage/src/rpc/channels.ts.html +376 -0
- package/coverage/src/rpc/cross-extension-rpc.ts.html +541 -0
- package/coverage/src/rpc/index.html +131 -0
- package/coverage/src/runtime/index.html +89 -29
- package/coverage/src/{goal-coordinator.ts.html → runtime/loop-events.ts.html} +79 -73
- package/coverage/src/runtime/monitor-ondone-runtime.ts.html +5 -5
- package/coverage/src/runtime/native-task-rpc.ts.html +502 -0
- package/coverage/src/runtime/notification-runtime.ts.html +14 -14
- package/coverage/src/runtime/scope.ts.html +17 -17
- package/coverage/src/runtime/session-runtime.ts.html +40 -40
- package/coverage/src/runtime/task-backlog-runtime.ts.html +66 -24
- package/coverage/src/runtime/task-events.ts.html +208 -0
- package/coverage/src/runtime/task-mutations.ts.html +394 -0
- package/coverage/src/runtime/task-rpc.ts.html +106 -118
- package/coverage/src/scheduler.ts.html +12 -12
- package/coverage/src/store.ts.html +20 -20
- package/coverage/src/task-backlog-coordinator.ts.html +8 -8
- package/coverage/src/task-reducer.ts.html +42 -42
- package/coverage/src/task-store.ts.html +49 -49
- package/coverage/src/tools/index.html +39 -24
- package/coverage/src/tools/loop-tools.ts.html +12 -21
- package/coverage/src/tools/monitor-tools.ts.html +12 -21
- package/coverage/src/tools/native-task-tools.ts.html +52 -94
- package/coverage/src/tools/tool-result.ts.html +97 -0
- package/coverage/src/trigger-system.ts.html +12 -12
- package/coverage/src/ui/index.html +1 -1
- package/coverage/src/ui/widget.ts.html +23 -23
- package/dist/api.d.ts +11 -0
- package/dist/api.js +10 -0
- package/dist/commands/loop-command.js +2 -2
- package/dist/commands/tasks-command.js +3 -3
- package/dist/coordinator.d.ts +1 -1
- package/dist/index.js +80 -20
- package/dist/loop-parse.js +14 -5
- package/dist/monitor-manager.d.ts +4 -1
- package/dist/monitor-manager.js +5 -3
- package/dist/reducer-backed-store.d.ts +1 -1
- package/dist/reducer-backed-store.js +1 -1
- package/dist/rpc/channels.d.ts +73 -0
- package/dist/rpc/channels.js +30 -0
- package/dist/rpc/cross-extension-rpc.d.ts +59 -0
- package/dist/rpc/cross-extension-rpc.js +117 -0
- package/dist/runtime/loop-events.d.ts +26 -0
- package/dist/runtime/loop-events.js +26 -0
- package/dist/runtime/native-task-rpc.d.ts +22 -0
- package/dist/runtime/native-task-rpc.js +50 -0
- package/dist/runtime/task-backlog-runtime.d.ts +3 -0
- package/dist/runtime/task-backlog-runtime.js +4 -1
- package/dist/runtime/task-mutations.d.ts +39 -0
- package/dist/runtime/task-mutations.js +68 -0
- package/dist/runtime/task-rpc.d.ts +4 -0
- package/dist/runtime/task-rpc.js +40 -58
- package/dist/scheduler.js +1 -1
- package/dist/tools/loop-tools.js +1 -3
- package/dist/tools/monitor-tools.js +1 -3
- package/dist/tools/native-task-tools.d.ts +2 -6
- package/dist/tools/native-task-tools.js +25 -55
- package/dist/tools/tool-result.d.ts +8 -0
- package/dist/tools/tool-result.js +4 -0
- package/docker-compose.yml +12 -0
- package/docs/architecture/state-machine-migration.md +0 -44
- package/docs/architecture/state-machine-reducer-event-model.md +2 -3
- package/package.json +6 -1
- package/release-please-config.json +9 -0
- package/src/api.ts +36 -0
- package/src/commands/loop-command.ts +2 -2
- package/src/commands/tasks-command.ts +3 -3
- package/src/coordinator.ts +1 -1
- package/src/index.ts +84 -22
- package/src/loop-parse.ts +18 -7
- package/src/monitor-manager.ts +11 -3
- package/src/reducer-backed-store.ts +3 -3
- package/src/rpc/channels.ts +97 -0
- package/src/rpc/cross-extension-rpc.ts +152 -0
- package/src/runtime/loop-events.ts +60 -0
- package/src/runtime/native-task-rpc.ts +139 -0
- package/src/runtime/task-backlog-runtime.ts +14 -0
- package/src/runtime/task-mutations.ts +103 -0
- package/src/runtime/task-rpc.ts +48 -54
- package/src/scheduler.ts +1 -1
- package/src/tools/loop-tools.ts +1 -4
- package/src/tools/monitor-tools.ts +1 -4
- package/src/tools/native-task-tools.ts +31 -52
- package/src/tools/tool-result.ts +4 -0
- package/src/trigger-system.ts +4 -4
- package/vitest.config.ts +11 -7
- package/coverage/src/goal-reducer.ts.html +0 -982
- package/coverage/src/goal-store.ts.html +0 -742
- package/coverage/src/goal-verifier.ts.html +0 -808
- package/dist/goal-coordinator.d.ts +0 -22
- package/dist/goal-coordinator.js +0 -28
- package/dist/goal-reducer.d.ts +0 -98
- package/dist/goal-reducer.js +0 -190
- package/dist/goal-store.d.ts +0 -22
- package/dist/goal-store.js +0 -188
- package/dist/goal-types.d.ts +0 -82
- package/dist/goal-types.js +0 -1
- package/dist/goal-verifier.d.ts +0 -20
- package/dist/goal-verifier.js +0 -198
- package/docs/architecture/goal-state-schema.md +0 -505
- package/src/goal-coordinator.ts +0 -58
- package/src/goal-reducer.ts +0 -299
- package/src/goal-store.ts +0 -219
- package/src/goal-types.ts +0 -104
- package/src/goal-verifier.ts +0 -241
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { createTask, deleteTask, updateTask, } from "../runtime/task-mutations.js";
|
|
3
|
+
import { textResult } from "./tool-result.js";
|
|
4
|
+
function backlogSuffix(backlog) {
|
|
5
|
+
return backlog.created && backlog.entry
|
|
6
|
+
? `\nBacklog worker loop #${backlog.entry.id} created`
|
|
7
|
+
: "";
|
|
5
8
|
}
|
|
6
9
|
export function registerNativeTaskTools(options) {
|
|
7
10
|
const { pi, taskStore, evaluateTaskBacklog, updateWidget } = options;
|
|
11
|
+
const mutationCtx = { pi, taskStore, evaluateTaskBacklog, updateWidget };
|
|
8
12
|
pi.registerTool({
|
|
9
13
|
name: "TaskCreate",
|
|
10
14
|
label: "TaskCreate",
|
|
@@ -30,14 +34,11 @@ Fields:
|
|
|
30
34
|
description: Type.String({ description: "Detailed description of what needs to be done" }),
|
|
31
35
|
}),
|
|
32
36
|
async execute(_toolCallId, params) {
|
|
33
|
-
const entry =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
? `\nBacklog worker loop #${backlog.entry.id} created`
|
|
39
|
-
: "";
|
|
40
|
-
return Promise.resolve(textResult(`Task #${entry.id} created: ${entry.subject}${autoLoopMsg}`));
|
|
37
|
+
const { entry, backlog } = await createTask(mutationCtx, {
|
|
38
|
+
subject: params.subject,
|
|
39
|
+
description: params.description,
|
|
40
|
+
});
|
|
41
|
+
return textResult(`Task #${entry.id} created: ${entry.subject}${backlogSuffix(backlog)}`);
|
|
41
42
|
},
|
|
42
43
|
});
|
|
43
44
|
pi.registerTool({
|
|
@@ -84,41 +85,16 @@ Parameters: id (required), status, subject, description`,
|
|
|
84
85
|
}),
|
|
85
86
|
async execute(_toolCallId, params) {
|
|
86
87
|
const { id, status, subject, description } = params;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
else if (status === "completed") {
|
|
97
|
-
entry = taskStore.complete(id);
|
|
98
|
-
if (entry)
|
|
99
|
-
emitNativeTaskEvent(pi, "tasks:completed", entry, previousStatus);
|
|
100
|
-
}
|
|
101
|
-
else if (status === "pending") {
|
|
102
|
-
entry = taskStore.reopen(id);
|
|
103
|
-
if (entry)
|
|
104
|
-
emitNativeTaskEvent(pi, "tasks:reopened", entry, previousStatus);
|
|
105
|
-
}
|
|
106
|
-
if (!entry)
|
|
107
|
-
return Promise.resolve(textResult(`Task #${id} not found`));
|
|
108
|
-
if (subject !== undefined || description !== undefined) {
|
|
109
|
-
entry = taskStore.updateDetails(id, { subject, description });
|
|
110
|
-
if (entry)
|
|
111
|
-
emitNativeTaskEvent(pi, "tasks:updated", entry, previousStatus);
|
|
112
|
-
}
|
|
113
|
-
if (!entry)
|
|
114
|
-
return Promise.resolve(textResult(`Task #${id} not found`));
|
|
115
|
-
updateWidget();
|
|
116
|
-
const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
88
|
+
const result = await updateTask(mutationCtx, {
|
|
89
|
+
id,
|
|
90
|
+
status: status,
|
|
91
|
+
subject,
|
|
92
|
+
description,
|
|
93
|
+
});
|
|
94
|
+
if (!result)
|
|
95
|
+
return textResult(`Task #${id} not found`);
|
|
117
96
|
const statusMsg = status ? ` → ${status}` : "";
|
|
118
|
-
|
|
119
|
-
? `\nBacklog worker loop #${backlog.entry.id} created`
|
|
120
|
-
: "";
|
|
121
|
-
return Promise.resolve(textResult(`Task #${id} updated${statusMsg}${autoLoopMsg}`));
|
|
97
|
+
return textResult(`Task #${id} updated${statusMsg}${backlogSuffix(result.backlog)}`);
|
|
122
98
|
},
|
|
123
99
|
});
|
|
124
100
|
pi.registerTool({
|
|
@@ -129,16 +105,10 @@ Parameters: id (required), status, subject, description`,
|
|
|
129
105
|
id: Type.String({ description: "Task ID to delete" }),
|
|
130
106
|
}),
|
|
131
107
|
async execute(_toolCallId, params) {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (existing)
|
|
137
|
-
emitNativeTaskEvent(pi, "tasks:deleted", existing, existing.status);
|
|
138
|
-
await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
139
|
-
return Promise.resolve(textResult(`Task #${params.id} deleted`));
|
|
140
|
-
}
|
|
141
|
-
return Promise.resolve(textResult(`Task #${params.id} not found`));
|
|
108
|
+
const result = await deleteTask(mutationCtx, params.id);
|
|
109
|
+
if (!result)
|
|
110
|
+
return textResult(`Task #${params.id} not found`);
|
|
111
|
+
return textResult(`Task #${params.id} deleted`);
|
|
142
112
|
},
|
|
143
113
|
});
|
|
144
114
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
services:
|
|
2
|
+
ci-node20:
|
|
3
|
+
build:
|
|
4
|
+
context: .
|
|
5
|
+
target: node20
|
|
6
|
+
command: sh -c "npm run typecheck && npm run lint && npm run test:coverage && npm run build"
|
|
7
|
+
|
|
8
|
+
ci-node22:
|
|
9
|
+
build:
|
|
10
|
+
context: .
|
|
11
|
+
target: node22
|
|
12
|
+
command: sh -c "npm run typecheck && npm run lint && npm run test:coverage && npm run build"
|
|
@@ -9,7 +9,6 @@ It is the capstone document for the architecture work captured in:
|
|
|
9
9
|
- `docs/architecture/state-machine-transition-map.md`
|
|
10
10
|
- `docs/architecture/state-machine-test-matrix.md`
|
|
11
11
|
- `docs/architecture/state-machine-reducer-event-model.md`
|
|
12
|
-
- `docs/architecture/goal-state-schema.md`
|
|
13
12
|
|
|
14
13
|
The goal of this document is to explain:
|
|
15
14
|
|
|
@@ -95,7 +94,6 @@ Instead, it uses separate reducer families for:
|
|
|
95
94
|
- loops
|
|
96
95
|
- notifications
|
|
97
96
|
- monitors
|
|
98
|
-
- later, goals
|
|
99
97
|
|
|
100
98
|
### 2.5 Name coordination rules explicitly
|
|
101
99
|
|
|
@@ -203,14 +201,6 @@ This defines:
|
|
|
203
201
|
- event vocabulary
|
|
204
202
|
- coordinator rules
|
|
205
203
|
|
|
206
|
-
### 4.4 Goal schema
|
|
207
|
-
|
|
208
|
-
`docs/architecture/goal-state-schema.md`
|
|
209
|
-
|
|
210
|
-
This defines the future Goal layer as a reducer-owned, verification-oriented composition of existing reducer state.
|
|
211
|
-
|
|
212
|
-
---
|
|
213
|
-
|
|
214
204
|
## 5. Implemented extraction status
|
|
215
205
|
|
|
216
206
|
At the time of this document, the following extractions are complete.
|
|
@@ -300,18 +290,6 @@ The coordinator currently exists as a reusable module with:
|
|
|
300
290
|
|
|
301
291
|
It is not yet fully wired through the main runtime.
|
|
302
292
|
|
|
303
|
-
### 5.6 Goal verifier prototype
|
|
304
|
-
|
|
305
|
-
Implemented:
|
|
306
|
-
|
|
307
|
-
- `src/goal-types.ts`
|
|
308
|
-
- `src/goal-verifier.ts`
|
|
309
|
-
- `test/goal-verifier.test.ts`
|
|
310
|
-
|
|
311
|
-
This is intentionally read-only and verification-oriented.
|
|
312
|
-
|
|
313
|
-
---
|
|
314
|
-
|
|
315
293
|
## 6. Current architecture after extraction
|
|
316
294
|
|
|
317
295
|
The codebase now sits in an intermediate but useful state.
|
|
@@ -321,7 +299,6 @@ The codebase now sits in an intermediate but useful state.
|
|
|
321
299
|
- entity state transitions are increasingly centralized in pure reducers
|
|
322
300
|
- transition semantics are backed by focused reducer tests
|
|
323
301
|
- scenario coverage exists for the highest-risk lifecycle behavior
|
|
324
|
-
- goal work no longer needs to start from an undefined state model
|
|
325
302
|
|
|
326
303
|
### 6.2 What remains transitional
|
|
327
304
|
|
|
@@ -380,7 +357,6 @@ Reducers own logical state transitions for:
|
|
|
380
357
|
- loops
|
|
381
358
|
- notifications
|
|
382
359
|
- monitors
|
|
383
|
-
- goals
|
|
384
360
|
|
|
385
361
|
### 7.3 Coordinator
|
|
386
362
|
|
|
@@ -424,10 +400,6 @@ Suggested order:
|
|
|
424
400
|
|
|
425
401
|
As each slice moves over, imperative code should become effect execution rather than state mutation.
|
|
426
402
|
|
|
427
|
-
### Step 4 — add a goal reducer
|
|
428
|
-
|
|
429
|
-
Once the coordinator is meaningfully integrated, add a true `reduceGoalState(...)` layer rather than keeping goal work verifier-only.
|
|
430
|
-
|
|
431
403
|
### Step 5 — remove redundant imperative state paths
|
|
432
404
|
|
|
433
405
|
Only after coordinator-owned dispatch is proven stable should old duplicated state paths be deleted.
|
|
@@ -476,20 +448,6 @@ Mitigation:
|
|
|
476
448
|
- only reduce logical state
|
|
477
449
|
- preserve stop/completion race tests
|
|
478
450
|
|
|
479
|
-
### 9.4 Goals can grow too ambitious too early
|
|
480
|
-
|
|
481
|
-
The goal prototype is intentionally read-only.
|
|
482
|
-
|
|
483
|
-
Risk:
|
|
484
|
-
|
|
485
|
-
- adding autonomous write-side goal behavior before coordinator adoption is mature
|
|
486
|
-
|
|
487
|
-
Mitigation:
|
|
488
|
-
|
|
489
|
-
- keep goal verification read-only until reducer/coordinator integration is stable
|
|
490
|
-
|
|
491
|
-
---
|
|
492
|
-
|
|
493
451
|
## 10. Validation strategy
|
|
494
452
|
|
|
495
453
|
The migration should continue to rely on three validation layers.
|
|
@@ -525,7 +483,6 @@ We now have:
|
|
|
525
483
|
- a test contract
|
|
526
484
|
- reducer modules for the four existing entity families
|
|
527
485
|
- a coordinator abstraction
|
|
528
|
-
- a goal schema and verifier prototype
|
|
529
486
|
|
|
530
487
|
This means future work can be judged against a coherent architecture instead of a collection of local fixes.
|
|
531
488
|
|
|
@@ -541,6 +498,5 @@ The system is not yet at the final architecture. However, it now has the essenti
|
|
|
541
498
|
- reducer state boundaries
|
|
542
499
|
- effect boundaries
|
|
543
500
|
- coordinator machinery
|
|
544
|
-
- verification-oriented goal design
|
|
545
501
|
|
|
546
502
|
The recommended next step, if further work continues, is to adopt the coordinator in narrow runtime slices rather than attempt a single large integration rewrite.
|
|
@@ -81,7 +81,7 @@ interface ReducerEvent<TType extends string = string, TPayload = unknown> {
|
|
|
81
81
|
| "session"
|
|
82
82
|
| "coordinator"
|
|
83
83
|
| "system";
|
|
84
|
-
entityType?: "task" | "loop" | "monitor" | "notification"
|
|
84
|
+
entityType?: "task" | "loop" | "monitor" | "notification";
|
|
85
85
|
entityId?: string;
|
|
86
86
|
payload: TPayload;
|
|
87
87
|
}
|
|
@@ -104,7 +104,7 @@ Reducers do not call Pi APIs directly. They emit effects.
|
|
|
104
104
|
```ts
|
|
105
105
|
interface ReducerEffect<TEffect extends string = string, TPayload = unknown> {
|
|
106
106
|
type: TEffect;
|
|
107
|
-
entityType?: "task" | "loop" | "monitor" | "notification"
|
|
107
|
+
entityType?: "task" | "loop" | "monitor" | "notification";
|
|
108
108
|
entityId?: string;
|
|
109
109
|
payload: TPayload;
|
|
110
110
|
}
|
|
@@ -801,7 +801,6 @@ If an event cannot be tied to a transition/invariant/scenario row, it is probabl
|
|
|
801
801
|
This event model does **not** yet define:
|
|
802
802
|
|
|
803
803
|
- Goal reducer fields
|
|
804
|
-
- persistent serialization format for goals
|
|
805
804
|
- UI rendering model
|
|
806
805
|
- task provider abstraction replacement
|
|
807
806
|
- a generalized event sourcing log
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trevonistrevon/pi-loop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A pi extension for cron/event-based agent re-wake loops and background process monitoring.",
|
|
5
5
|
"author": "trevonistrevon",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
"monitor",
|
|
18
18
|
"scheduler"
|
|
19
19
|
],
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./src/index.ts",
|
|
22
|
+
"./api": "./src/api.ts",
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
20
25
|
"peerDependencies": {
|
|
21
26
|
"@earendil-works/pi-coding-agent": "^0.75.5",
|
|
22
27
|
"@earendil-works/pi-tui": "^0.75.5"
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API surface for sibling extensions (imported as
|
|
3
|
+
* `@trevonistrevon/pi-loop/api`). Everything else under src/ is internal —
|
|
4
|
+
* the package `exports` map deliberately blocks deep imports.
|
|
5
|
+
*/
|
|
6
|
+
export {
|
|
7
|
+
type CleanReply,
|
|
8
|
+
type CreateTaskParams,
|
|
9
|
+
type CreateTaskReply,
|
|
10
|
+
type PendingReply,
|
|
11
|
+
type PingReply,
|
|
12
|
+
replyChannel,
|
|
13
|
+
type SpawnParams,
|
|
14
|
+
type SpawnReply,
|
|
15
|
+
SUBAGENTS_RPC,
|
|
16
|
+
TASK_EVENTS,
|
|
17
|
+
TASKS_RPC,
|
|
18
|
+
type TaskEntryWire,
|
|
19
|
+
type TaskStatusWire,
|
|
20
|
+
type UpdateTaskParams,
|
|
21
|
+
type UpdateTaskReply,
|
|
22
|
+
} from "./rpc/channels.js";
|
|
23
|
+
export {
|
|
24
|
+
type HandleRpcOptions,
|
|
25
|
+
handleRpc,
|
|
26
|
+
PROTOCOL_VERSION,
|
|
27
|
+
RpcError,
|
|
28
|
+
type RpcEventBus,
|
|
29
|
+
type RpcReply,
|
|
30
|
+
rpcCall,
|
|
31
|
+
rpcProbe,
|
|
32
|
+
} from "./rpc/cross-extension-rpc.js";
|
|
33
|
+
export { NATIVE_TASKS_PROVIDER } from "./runtime/native-task-rpc.js";
|
|
34
|
+
export { resolveLoopStorePath, resolveTaskStorePath } from "./runtime/scope.js";
|
|
35
|
+
export { TaskStore } from "./task-store.js";
|
|
36
|
+
export type { TaskEntry, TaskStatus, TaskStoreData } from "./task-types.js";
|
|
@@ -85,7 +85,7 @@ export function registerLoopCommand(options: LoopCommandOptions): void {
|
|
|
85
85
|
if (!selected || selected === "< Back") return;
|
|
86
86
|
|
|
87
87
|
const match = selected.match(/#(\d+)/);
|
|
88
|
-
if (match) {
|
|
88
|
+
if (match?.[1]) {
|
|
89
89
|
const entry = getStore().get(match[1]);
|
|
90
90
|
if (entry) {
|
|
91
91
|
const actions = ["x Delete"];
|
|
@@ -149,7 +149,7 @@ export function registerLoopCommand(options: LoopCommandOptions): void {
|
|
|
149
149
|
|
|
150
150
|
const intervalMatch = trimmed.match(/^(\d+\s*[smhdS]\b)/i);
|
|
151
151
|
if (intervalMatch) {
|
|
152
|
-
const interval = intervalMatch[1];
|
|
152
|
+
const interval = intervalMatch[1] ?? intervalMatch[0];
|
|
153
153
|
const prompt = trimmed.slice(intervalMatch[0].length).trim();
|
|
154
154
|
|
|
155
155
|
if (!prompt) {
|
|
@@ -67,10 +67,10 @@ export function registerTasksCommand(options: TasksCommandOptions): void {
|
|
|
67
67
|
return viewNativeTasks(ui);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
const
|
|
71
|
-
if (!
|
|
70
|
+
const taskId = selected.match(/#(\d+)/)?.[1];
|
|
71
|
+
if (!taskId) return viewNativeTasks(ui);
|
|
72
72
|
|
|
73
|
-
const task = taskStore.get(
|
|
73
|
+
const task = taskStore.get(taskId);
|
|
74
74
|
if (!task) return viewNativeTasks(ui);
|
|
75
75
|
|
|
76
76
|
const actions = ["x Delete"];
|
package/src/coordinator.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type ReducerSource =
|
|
|
8
8
|
| "coordinator"
|
|
9
9
|
| "system";
|
|
10
10
|
|
|
11
|
-
export type ReducerEntityType = "task" | "loop" | "monitor" | "notification"
|
|
11
|
+
export type ReducerEntityType = "task" | "loop" | "monitor" | "notification";
|
|
12
12
|
|
|
13
13
|
export interface ReducerEvent<TType extends string = string, TPayload = unknown> {
|
|
14
14
|
type: TType;
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { registerTasksCommand } from "./commands/tasks-command.js";
|
|
|
20
20
|
import { atMaxFires } from "./loop-reducer.js";
|
|
21
21
|
import { MonitorManager } from "./monitor-manager.js";
|
|
22
22
|
import { createMonitorOnDoneRuntime } from "./runtime/monitor-ondone-runtime.js";
|
|
23
|
+
import { registerNativeTaskRpc } from "./runtime/native-task-rpc.js";
|
|
23
24
|
import {
|
|
24
25
|
createNotificationRuntime,
|
|
25
26
|
type LoopFireEvent,
|
|
@@ -43,6 +44,10 @@ function debug(...args: unknown[]) {
|
|
|
43
44
|
if (DEBUG) console.error("[pi-loop]", ...args);
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
function isStaleExtensionContextError(error: unknown): boolean {
|
|
48
|
+
return error instanceof Error && error.message.includes("extension ctx is stale");
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
export default function (pi: ExtensionAPI) {
|
|
47
52
|
const piLoopEnv = process.env.PI_LOOP;
|
|
48
53
|
const piLoopScope = process.env.PI_LOOP_SCOPE as "memory" | "session" | "project" | undefined;
|
|
@@ -59,7 +64,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
59
64
|
// call), so stale monitors don't linger in the count between turns.
|
|
60
65
|
monitorManager.setOnChange(() => widget.update());
|
|
61
66
|
widget.setTaskSummaryProvider(() => {
|
|
62
|
-
|
|
67
|
+
// Once pi-tasks owns the channels, any native store created during the
|
|
68
|
+
// detection window is a shadow — never surface it in the status line.
|
|
69
|
+
if (tasksAvailable || !nativeTaskStore) return { count: 0 };
|
|
63
70
|
const tasks = nativeTaskStore.list().filter(t => t.status === "pending" || t.status === "in_progress");
|
|
64
71
|
const active = tasks.find(t => t.status === "in_progress");
|
|
65
72
|
const next = tasks.find(t => t.status === "pending");
|
|
@@ -76,9 +83,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
76
83
|
|
|
77
84
|
// ── pi-tasks integration ──
|
|
78
85
|
let tasksAvailable = false;
|
|
86
|
+
let tasksDetectionSettled = false;
|
|
79
87
|
let nativeTaskStore: TaskStore | undefined;
|
|
80
88
|
let nativeTasksRegistered = false;
|
|
81
89
|
|
|
90
|
+
function getOrCreateNativeTaskStore(): TaskStore | undefined {
|
|
91
|
+
// pi-tasks owns the task channels; don't create a shadow store.
|
|
92
|
+
if (tasksAvailable) return undefined;
|
|
93
|
+
if (!nativeTaskStore) {
|
|
94
|
+
nativeTaskStore = new TaskStore(resolveTaskStorePath(getScopeOptions(), _sessionId));
|
|
95
|
+
}
|
|
96
|
+
return nativeTaskStore;
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
const taskRuntime = createTaskRuntimeBridge({
|
|
83
100
|
pi,
|
|
84
101
|
isTasksAvailable: () => tasksAvailable,
|
|
@@ -93,6 +110,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
93
110
|
widget.update();
|
|
94
111
|
await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
95
112
|
},
|
|
113
|
+
onDetectionStarted: () => {
|
|
114
|
+
tasksDetectionSettled = false;
|
|
115
|
+
},
|
|
116
|
+
onDetectionSettled: () => {
|
|
117
|
+
tasksDetectionSettled = true;
|
|
118
|
+
},
|
|
119
|
+
debug,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// The RPC server registers at init (not behind the 6s tool-fallback timer) so
|
|
123
|
+
// early cross-extension calls never race the timer; it stands down via
|
|
124
|
+
// isEnabled once an external pi-tasks is detected. checkTasksVersion ignores
|
|
125
|
+
// this server's own ping reply via its provider field. Mutating verbs stay
|
|
126
|
+
// silent until the detection probe settles, so a co-resident pi-tasks can
|
|
127
|
+
// never race the native server into creating divergent task state.
|
|
128
|
+
registerNativeTaskRpc({
|
|
129
|
+
pi,
|
|
130
|
+
getNativeTaskStore: getOrCreateNativeTaskStore,
|
|
131
|
+
isEnabled: () => !tasksAvailable,
|
|
132
|
+
isDetectionSettled: () => tasksDetectionSettled,
|
|
133
|
+
evaluateTaskBacklog: (taskStore, pendingCount) =>
|
|
134
|
+
evaluateTaskBacklog(taskStore, pendingCount),
|
|
135
|
+
updateWidget: () => {
|
|
136
|
+
widget.update();
|
|
137
|
+
},
|
|
96
138
|
debug,
|
|
97
139
|
});
|
|
98
140
|
|
|
@@ -169,6 +211,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
169
211
|
hasPendingTasks: () => hasPendingTasks(),
|
|
170
212
|
bootstrapTaskLoop: (entry) => maybeBootstrapTaskLoop(entry),
|
|
171
213
|
triggerHasEventSource,
|
|
214
|
+
emitLoopAutodeleted: (payload) => {
|
|
215
|
+
pi.events.emit("loops:autodeleted", payload);
|
|
216
|
+
},
|
|
217
|
+
emitTaskBacklogEmpty: (payload) => {
|
|
218
|
+
pi.events.emit("tasks:backlog_empty", payload);
|
|
219
|
+
},
|
|
172
220
|
debug,
|
|
173
221
|
});
|
|
174
222
|
|
|
@@ -297,30 +345,44 @@ export default function (pi: ExtensionAPI) {
|
|
|
297
345
|
|
|
298
346
|
// ── Native task tools (only when pi-tasks is absent) ──
|
|
299
347
|
|
|
300
|
-
|
|
348
|
+
// Only tool/command registration stays deferred: the tool names collide with
|
|
349
|
+
// pi-tasks and cannot be unregistered. The RPC server is already live (above).
|
|
350
|
+
const nativeTaskFallbackTimer = setTimeout(() => {
|
|
301
351
|
if (tasksAvailable || nativeTasksRegistered) return;
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
352
|
+
const taskStore = getOrCreateNativeTaskStore();
|
|
353
|
+
if (!taskStore) return;
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
registerTasksCommand({
|
|
357
|
+
pi,
|
|
358
|
+
getNativeTaskStore: () => nativeTaskStore,
|
|
359
|
+
evaluateTaskBacklog,
|
|
360
|
+
updateWidget: () => {
|
|
361
|
+
widget.update();
|
|
362
|
+
},
|
|
363
|
+
});
|
|
314
364
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
365
|
+
registerNativeTaskTools({
|
|
366
|
+
pi,
|
|
367
|
+
taskStore,
|
|
368
|
+
evaluateTaskBacklog,
|
|
369
|
+
updateWidget: () => {
|
|
370
|
+
widget.update();
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (isStaleExtensionContextError(error)) {
|
|
375
|
+
debug("native task fallback skipped: extension context went stale");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
323
380
|
|
|
381
|
+
nativeTasksRegistered = true;
|
|
324
382
|
debug("native task tools registered (pi-tasks not detected)");
|
|
325
383
|
}, 6000);
|
|
384
|
+
|
|
385
|
+
pi.on("session_shutdown", () => {
|
|
386
|
+
clearTimeout(nativeTaskFallbackTimer);
|
|
387
|
+
});
|
|
326
388
|
}
|
package/src/loop-parse.ts
CHANGED
|
@@ -23,8 +23,10 @@ const COMMON_INTERVALS: Record<number, string> = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
function roundToNearestCommon(seconds: number): { cron: string; description: string } {
|
|
26
|
+
// COMMON_INTERVALS is a non-empty const table, so keys[0] and
|
|
27
|
+
// COMMON_INTERVALS[best] below are true invariants, not runtime fallbacks.
|
|
26
28
|
const keys = Object.keys(COMMON_INTERVALS).map(Number).sort((a, b) => a - b);
|
|
27
|
-
let best = keys[0];
|
|
29
|
+
let best = keys[0] as number;
|
|
28
30
|
for (const k of keys) {
|
|
29
31
|
if (Math.abs(k - seconds) < Math.abs(best - seconds)) best = k;
|
|
30
32
|
}
|
|
@@ -43,7 +45,7 @@ function roundToNearestCommon(seconds: number): { cron: string; description: str
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
return { cron: COMMON_INTERVALS[best], description };
|
|
48
|
+
return { cron: COMMON_INTERVALS[best] as string, description };
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
function isFullCron(expr: string): boolean {
|
|
@@ -60,8 +62,8 @@ export function parseInterval(input: string): { cron: string; description: strin
|
|
|
60
62
|
|
|
61
63
|
const match = trimmed.match(/^(\d+)\s*(s|m|h|d)$/i);
|
|
62
64
|
if (match) {
|
|
63
|
-
const value = parseInt(match[1], 10);
|
|
64
|
-
const unit = match[2].toLowerCase();
|
|
65
|
+
const value = parseInt(match[1] ?? "", 10);
|
|
66
|
+
const unit = (match[2] ?? "").toLowerCase();
|
|
65
67
|
const totalSec = value * (UNIT_TO_CRON[unit] ?? 60);
|
|
66
68
|
|
|
67
69
|
if (totalSec < 60) {
|
|
@@ -81,6 +83,15 @@ export function cronToNextFire(cronExpr: string, fromDate: Date = new Date()): D
|
|
|
81
83
|
if (parts.length !== 5) throw new Error(`Invalid cron expression: ${cronExpr}`);
|
|
82
84
|
|
|
83
85
|
const [minF, hourF, dayF, monthF, dowF] = parts;
|
|
86
|
+
if (
|
|
87
|
+
minF === undefined ||
|
|
88
|
+
hourF === undefined ||
|
|
89
|
+
dayF === undefined ||
|
|
90
|
+
monthF === undefined ||
|
|
91
|
+
dowF === undefined
|
|
92
|
+
) {
|
|
93
|
+
throw new Error(`Invalid cron expression: ${cronExpr}`);
|
|
94
|
+
}
|
|
84
95
|
const now = new Date(fromDate);
|
|
85
96
|
now.setSeconds(0, 0);
|
|
86
97
|
|
|
@@ -107,7 +118,7 @@ function cronFieldMatches(field: string, value: number): boolean {
|
|
|
107
118
|
if (part === "*") return true;
|
|
108
119
|
|
|
109
120
|
if (part.includes("/")) {
|
|
110
|
-
const [range, stepStr] = part.split("/");
|
|
121
|
+
const [range = "", stepStr = ""] = part.split("/");
|
|
111
122
|
const step = parseInt(stepStr, 10);
|
|
112
123
|
let rangeMin: number;
|
|
113
124
|
let rangeMax: number;
|
|
@@ -116,7 +127,7 @@ function cronFieldMatches(field: string, value: number): boolean {
|
|
|
116
127
|
rangeMin = 0;
|
|
117
128
|
rangeMax = 59;
|
|
118
129
|
} else if (range.includes("-")) {
|
|
119
|
-
const [minS, maxS] = range.split("-");
|
|
130
|
+
const [minS = "", maxS = ""] = range.split("-");
|
|
120
131
|
rangeMin = parseInt(minS, 10);
|
|
121
132
|
rangeMax = parseInt(maxS, 10);
|
|
122
133
|
} else {
|
|
@@ -132,7 +143,7 @@ function cronFieldMatches(field: string, value: number): boolean {
|
|
|
132
143
|
}
|
|
133
144
|
|
|
134
145
|
if (part.includes("-")) {
|
|
135
|
-
const [minS, maxS] = part.split("-");
|
|
146
|
+
const [minS = "", maxS = ""] = part.split("-");
|
|
136
147
|
const min = parseInt(minS, 10);
|
|
137
148
|
const max = parseInt(maxS, 10);
|
|
138
149
|
if (value >= min && value <= max) return true;
|
package/src/monitor-manager.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
2
|
+
import { type ChildProcess, spawn as nodeSpawn, type SpawnOptions } from "node:child_process";
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import {
|
|
5
5
|
type MonitorReducerEvent,
|
|
@@ -8,12 +8,20 @@ import {
|
|
|
8
8
|
} from "./monitor-reducer.js";
|
|
9
9
|
import type { MonitorEntry, MonitorProcess } from "./types.js";
|
|
10
10
|
|
|
11
|
+
export type SpawnFn = (command: string, args: string[], options: SpawnOptions) => ChildProcess;
|
|
12
|
+
|
|
11
13
|
export class MonitorManager {
|
|
12
14
|
private processes = new Map<string, MonitorProcess>();
|
|
13
15
|
private nextId = 1;
|
|
14
16
|
private onChange: (() => void) | undefined;
|
|
17
|
+
private spawnFn: SpawnFn;
|
|
15
18
|
|
|
16
|
-
constructor(
|
|
19
|
+
constructor(
|
|
20
|
+
private pi: ExtensionAPI,
|
|
21
|
+
spawnFn?: SpawnFn,
|
|
22
|
+
) {
|
|
23
|
+
this.spawnFn = spawnFn ?? ((cmd, args, opts) => nodeSpawn(cmd, args, opts));
|
|
24
|
+
}
|
|
17
25
|
|
|
18
26
|
/**
|
|
19
27
|
* Register a callback fired when a monitor's status changes or it is pruned
|
|
@@ -90,7 +98,7 @@ export class MonitorManager {
|
|
|
90
98
|
const entry = result.state.monitorsById[id]!;
|
|
91
99
|
|
|
92
100
|
const abortController = new AbortController();
|
|
93
|
-
const child =
|
|
101
|
+
const child = this.spawnFn("sh", ["-c", command], {
|
|
94
102
|
stdio: ["ignore", "pipe", "pipe"],
|
|
95
103
|
signal: abortController.signal,
|
|
96
104
|
env: { ...process.env },
|