@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
|
@@ -147,44 +147,44 @@
|
|
|
147
147
|
<span class="cline-any cline-neutral"> </span>
|
|
148
148
|
<span class="cline-any cline-neutral"> </span>
|
|
149
149
|
<span class="cline-any cline-neutral"> </span>
|
|
150
|
-
<span class="cline-any cline-yes">
|
|
151
|
-
<span class="cline-any cline-yes">
|
|
150
|
+
<span class="cline-any cline-yes">72x</span>
|
|
151
|
+
<span class="cline-any cline-yes">72x</span>
|
|
152
152
|
<span class="cline-any cline-neutral"> </span>
|
|
153
153
|
<span class="cline-any cline-neutral"> </span>
|
|
154
154
|
<span class="cline-any cline-neutral"> </span>
|
|
155
|
-
<span class="cline-any cline-yes">
|
|
155
|
+
<span class="cline-any cline-yes">108x</span>
|
|
156
156
|
<span class="cline-any cline-neutral"> </span>
|
|
157
157
|
<span class="cline-any cline-neutral"> </span>
|
|
158
158
|
<span class="cline-any cline-neutral"> </span>
|
|
159
|
-
<span class="cline-any cline-yes">
|
|
159
|
+
<span class="cline-any cline-yes">47x</span>
|
|
160
160
|
<span class="cline-any cline-neutral"> </span>
|
|
161
161
|
<span class="cline-any cline-neutral"> </span>
|
|
162
162
|
<span class="cline-any cline-neutral"> </span>
|
|
163
|
-
<span class="cline-any cline-yes">
|
|
163
|
+
<span class="cline-any cline-yes">67x</span>
|
|
164
164
|
<span class="cline-any cline-neutral"> </span>
|
|
165
165
|
<span class="cline-any cline-neutral"> </span>
|
|
166
166
|
<span class="cline-any cline-neutral"> </span>
|
|
167
|
-
<span class="cline-any cline-yes">
|
|
168
|
-
<span class="cline-any cline-yes">
|
|
167
|
+
<span class="cline-any cline-yes">191x</span>
|
|
168
|
+
<span class="cline-any cline-yes">168x</span>
|
|
169
169
|
<span class="cline-any cline-neutral"> </span>
|
|
170
170
|
<span class="cline-any cline-neutral"> </span>
|
|
171
171
|
<span class="cline-any cline-neutral"> </span>
|
|
172
|
-
<span class="cline-any cline-yes">
|
|
173
|
-
<span class="cline-any cline-yes">
|
|
174
|
-
<span class="cline-any cline-yes">
|
|
172
|
+
<span class="cline-any cline-yes">168x</span>
|
|
173
|
+
<span class="cline-any cline-yes">168x</span>
|
|
174
|
+
<span class="cline-any cline-yes">168x</span>
|
|
175
175
|
<span class="cline-any cline-neutral"> </span>
|
|
176
|
-
<span class="cline-any cline-yes">
|
|
177
|
-
<span class="cline-any cline-yes">
|
|
176
|
+
<span class="cline-any cline-yes">168x</span>
|
|
177
|
+
<span class="cline-any cline-yes">70x</span>
|
|
178
178
|
<span class="cline-any cline-neutral"> </span>
|
|
179
179
|
<span class="cline-any cline-neutral"> </span>
|
|
180
|
-
<span class="cline-any cline-yes">
|
|
181
|
-
<span class="cline-any cline-yes">
|
|
182
|
-
<span class="cline-any cline-yes">
|
|
183
|
-
<span class="cline-any cline-yes">
|
|
180
|
+
<span class="cline-any cline-yes">98x</span>
|
|
181
|
+
<span class="cline-any cline-yes">98x</span>
|
|
182
|
+
<span class="cline-any cline-yes">98x</span>
|
|
183
|
+
<span class="cline-any cline-yes">98x</span>
|
|
184
184
|
<span class="cline-any cline-neutral"> </span>
|
|
185
|
-
<span class="cline-any cline-yes">
|
|
186
|
-
<span class="cline-any cline-yes">
|
|
187
|
-
<span class="cline-any cline-yes">
|
|
185
|
+
<span class="cline-any cline-yes">98x</span>
|
|
186
|
+
<span class="cline-any cline-yes">98x</span>
|
|
187
|
+
<span class="cline-any cline-yes">98x</span>
|
|
188
188
|
<span class="cline-any cline-neutral"> </span>
|
|
189
189
|
<span class="cline-any cline-neutral"> </span>
|
|
190
190
|
<span class="cline-any cline-neutral"> </span>
|
|
@@ -193,12 +193,12 @@
|
|
|
193
193
|
<span class="cline-any cline-neutral"> </span>
|
|
194
194
|
<span class="cline-any cline-neutral"> </span>
|
|
195
195
|
<span class="cline-any cline-neutral"> </span>
|
|
196
|
-
<span class="cline-any cline-yes">
|
|
196
|
+
<span class="cline-any cline-yes">117x</span>
|
|
197
197
|
<span class="cline-any cline-neutral"> </span>
|
|
198
198
|
<span class="cline-any cline-neutral"> </span>
|
|
199
199
|
<span class="cline-any cline-neutral"> </span>
|
|
200
|
-
<span class="cline-any cline-yes">
|
|
201
|
-
<span class="cline-any cline-yes">
|
|
200
|
+
<span class="cline-any cline-yes">40x</span>
|
|
201
|
+
<span class="cline-any cline-yes">39x</span>
|
|
202
202
|
<span class="cline-any cline-yes">6x</span>
|
|
203
203
|
<span class="cline-any cline-neutral"> </span>
|
|
204
204
|
<span class="cline-any cline-neutral"> </span></td><td class="text"><pre class="prettyprint lang-js">import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -277,7 +277,7 @@ function isStatusVisibleLoop(loop: LoopEntry): boolean {
|
|
|
277
277
|
<div class='footer quiet pad2 space-top1 center small'>
|
|
278
278
|
Code coverage generated by
|
|
279
279
|
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
|
|
280
|
-
at 2026-
|
|
280
|
+
at 2026-07-02T19:31:50.067Z
|
|
281
281
|
</div>
|
|
282
282
|
<script src="../../prettify.js"></script>
|
|
283
283
|
<script>
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
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 { type CleanReply, type CreateTaskParams, type CreateTaskReply, type PendingReply, type PingReply, replyChannel, type SpawnParams, type SpawnReply, SUBAGENTS_RPC, TASK_EVENTS, TASKS_RPC, type TaskEntryWire, type TaskStatusWire, type UpdateTaskParams, type UpdateTaskReply, } from "./rpc/channels.js";
|
|
7
|
+
export { type HandleRpcOptions, handleRpc, PROTOCOL_VERSION, RpcError, type RpcEventBus, type RpcReply, rpcCall, rpcProbe, } from "./rpc/cross-extension-rpc.js";
|
|
8
|
+
export { NATIVE_TASKS_PROVIDER } from "./runtime/native-task-rpc.js";
|
|
9
|
+
export { resolveLoopStorePath, resolveTaskStorePath } from "./runtime/scope.js";
|
|
10
|
+
export { TaskStore } from "./task-store.js";
|
|
11
|
+
export type { TaskEntry, TaskStatus, TaskStoreData } from "./task-types.js";
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
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 { replyChannel, SUBAGENTS_RPC, TASK_EVENTS, TASKS_RPC, } from "./rpc/channels.js";
|
|
7
|
+
export { handleRpc, PROTOCOL_VERSION, RpcError, rpcCall, rpcProbe, } from "./rpc/cross-extension-rpc.js";
|
|
8
|
+
export { NATIVE_TASKS_PROVIDER } from "./runtime/native-task-rpc.js";
|
|
9
|
+
export { resolveLoopStorePath, resolveTaskStorePath } from "./runtime/scope.js";
|
|
10
|
+
export { TaskStore } from "./task-store.js";
|
|
@@ -53,7 +53,7 @@ export function registerLoopCommand(options) {
|
|
|
53
53
|
if (!selected || selected === "< Back")
|
|
54
54
|
return;
|
|
55
55
|
const match = selected.match(/#(\d+)/);
|
|
56
|
-
if (match) {
|
|
56
|
+
if (match?.[1]) {
|
|
57
57
|
const entry = getStore().get(match[1]);
|
|
58
58
|
if (entry) {
|
|
59
59
|
const actions = ["x Delete"];
|
|
@@ -114,7 +114,7 @@ export function registerLoopCommand(options) {
|
|
|
114
114
|
}
|
|
115
115
|
const intervalMatch = trimmed.match(/^(\d+\s*[smhdS]\b)/i);
|
|
116
116
|
if (intervalMatch) {
|
|
117
|
-
const interval = intervalMatch[1];
|
|
117
|
+
const interval = intervalMatch[1] ?? intervalMatch[0];
|
|
118
118
|
const prompt = trimmed.slice(intervalMatch[0].length).trim();
|
|
119
119
|
if (!prompt) {
|
|
120
120
|
ui.notify("Provide a prompt after the interval, e.g., /loop 5m check the deploy", "warning");
|
|
@@ -47,10 +47,10 @@ export function registerTasksCommand(options) {
|
|
|
47
47
|
await createNativeTaskInteractively(ui);
|
|
48
48
|
return viewNativeTasks(ui);
|
|
49
49
|
}
|
|
50
|
-
const
|
|
51
|
-
if (!
|
|
50
|
+
const taskId = selected.match(/#(\d+)/)?.[1];
|
|
51
|
+
if (!taskId)
|
|
52
52
|
return viewNativeTasks(ui);
|
|
53
|
-
const task = taskStore.get(
|
|
53
|
+
const task = taskStore.get(taskId);
|
|
54
54
|
if (!task)
|
|
55
55
|
return viewNativeTasks(ui);
|
|
56
56
|
const actions = ["x Delete"];
|
package/dist/coordinator.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
2
|
-
export type ReducerEntityType = "task" | "loop" | "monitor" | "notification"
|
|
2
|
+
export type ReducerEntityType = "task" | "loop" | "monitor" | "notification";
|
|
3
3
|
export interface ReducerEvent<TType extends string = string, TPayload = unknown> {
|
|
4
4
|
type: TType;
|
|
5
5
|
at: number;
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { registerTasksCommand } from "./commands/tasks-command.js";
|
|
|
18
18
|
import { atMaxFires } from "./loop-reducer.js";
|
|
19
19
|
import { MonitorManager } from "./monitor-manager.js";
|
|
20
20
|
import { createMonitorOnDoneRuntime } from "./runtime/monitor-ondone-runtime.js";
|
|
21
|
+
import { registerNativeTaskRpc } from "./runtime/native-task-rpc.js";
|
|
21
22
|
import { createNotificationRuntime, } from "./runtime/notification-runtime.js";
|
|
22
23
|
import { resolveLoopStorePath, resolveTaskStorePath } from "./runtime/scope.js";
|
|
23
24
|
import { registerSessionRuntimeHooks } from "./runtime/session-runtime.js";
|
|
@@ -36,6 +37,9 @@ function debug(...args) {
|
|
|
36
37
|
if (DEBUG)
|
|
37
38
|
console.error("[pi-loop]", ...args);
|
|
38
39
|
}
|
|
40
|
+
function isStaleExtensionContextError(error) {
|
|
41
|
+
return error instanceof Error && error.message.includes("extension ctx is stale");
|
|
42
|
+
}
|
|
39
43
|
export default function (pi) {
|
|
40
44
|
const piLoopEnv = process.env.PI_LOOP;
|
|
41
45
|
const piLoopScope = process.env.PI_LOOP_SCOPE;
|
|
@@ -50,7 +54,9 @@ export default function (pi) {
|
|
|
50
54
|
// call), so stale monitors don't linger in the count between turns.
|
|
51
55
|
monitorManager.setOnChange(() => widget.update());
|
|
52
56
|
widget.setTaskSummaryProvider(() => {
|
|
53
|
-
|
|
57
|
+
// Once pi-tasks owns the channels, any native store created during the
|
|
58
|
+
// detection window is a shadow — never surface it in the status line.
|
|
59
|
+
if (tasksAvailable || !nativeTaskStore)
|
|
54
60
|
return { count: 0 };
|
|
55
61
|
const tasks = nativeTaskStore.list().filter(t => t.status === "pending" || t.status === "in_progress");
|
|
56
62
|
const active = tasks.find(t => t.status === "in_progress");
|
|
@@ -66,8 +72,18 @@ export default function (pi) {
|
|
|
66
72
|
triggerSystem = new TriggerSystem(pi, scheduler, store, onLoopFire);
|
|
67
73
|
// ── pi-tasks integration ──
|
|
68
74
|
let tasksAvailable = false;
|
|
75
|
+
let tasksDetectionSettled = false;
|
|
69
76
|
let nativeTaskStore;
|
|
70
77
|
let nativeTasksRegistered = false;
|
|
78
|
+
function getOrCreateNativeTaskStore() {
|
|
79
|
+
// pi-tasks owns the task channels; don't create a shadow store.
|
|
80
|
+
if (tasksAvailable)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (!nativeTaskStore) {
|
|
83
|
+
nativeTaskStore = new TaskStore(resolveTaskStorePath(getScopeOptions(), _sessionId));
|
|
84
|
+
}
|
|
85
|
+
return nativeTaskStore;
|
|
86
|
+
}
|
|
71
87
|
const taskRuntime = createTaskRuntimeBridge({
|
|
72
88
|
pi,
|
|
73
89
|
isTasksAvailable: () => tasksAvailable,
|
|
@@ -83,6 +99,29 @@ export default function (pi) {
|
|
|
83
99
|
widget.update();
|
|
84
100
|
await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
85
101
|
},
|
|
102
|
+
onDetectionStarted: () => {
|
|
103
|
+
tasksDetectionSettled = false;
|
|
104
|
+
},
|
|
105
|
+
onDetectionSettled: () => {
|
|
106
|
+
tasksDetectionSettled = true;
|
|
107
|
+
},
|
|
108
|
+
debug,
|
|
109
|
+
});
|
|
110
|
+
// The RPC server registers at init (not behind the 6s tool-fallback timer) so
|
|
111
|
+
// early cross-extension calls never race the timer; it stands down via
|
|
112
|
+
// isEnabled once an external pi-tasks is detected. checkTasksVersion ignores
|
|
113
|
+
// this server's own ping reply via its provider field. Mutating verbs stay
|
|
114
|
+
// silent until the detection probe settles, so a co-resident pi-tasks can
|
|
115
|
+
// never race the native server into creating divergent task state.
|
|
116
|
+
registerNativeTaskRpc({
|
|
117
|
+
pi,
|
|
118
|
+
getNativeTaskStore: getOrCreateNativeTaskStore,
|
|
119
|
+
isEnabled: () => !tasksAvailable,
|
|
120
|
+
isDetectionSettled: () => tasksDetectionSettled,
|
|
121
|
+
evaluateTaskBacklog: (taskStore, pendingCount) => evaluateTaskBacklog(taskStore, pendingCount),
|
|
122
|
+
updateWidget: () => {
|
|
123
|
+
widget.update();
|
|
124
|
+
},
|
|
86
125
|
debug,
|
|
87
126
|
});
|
|
88
127
|
taskRuntime.checkTasksVersion();
|
|
@@ -153,6 +192,12 @@ export default function (pi) {
|
|
|
153
192
|
hasPendingTasks: () => hasPendingTasks(),
|
|
154
193
|
bootstrapTaskLoop: (entry) => maybeBootstrapTaskLoop(entry),
|
|
155
194
|
triggerHasEventSource,
|
|
195
|
+
emitLoopAutodeleted: (payload) => {
|
|
196
|
+
pi.events.emit("loops:autodeleted", payload);
|
|
197
|
+
},
|
|
198
|
+
emitTaskBacklogEmpty: (payload) => {
|
|
199
|
+
pi.events.emit("tasks:backlog_empty", payload);
|
|
200
|
+
},
|
|
156
201
|
debug,
|
|
157
202
|
});
|
|
158
203
|
const flushPendingNotifications = notificationRuntime.flushPendingNotifications;
|
|
@@ -263,28 +308,43 @@ export default function (pi) {
|
|
|
263
308
|
},
|
|
264
309
|
});
|
|
265
310
|
// ── Native task tools (only when pi-tasks is absent) ──
|
|
266
|
-
|
|
311
|
+
// Only tool/command registration stays deferred: the tool names collide with
|
|
312
|
+
// pi-tasks and cannot be unregistered. The RPC server is already live (above).
|
|
313
|
+
const nativeTaskFallbackTimer = setTimeout(() => {
|
|
267
314
|
if (tasksAvailable || nativeTasksRegistered)
|
|
268
315
|
return;
|
|
269
|
-
|
|
316
|
+
const taskStore = getOrCreateNativeTaskStore();
|
|
317
|
+
if (!taskStore)
|
|
318
|
+
return;
|
|
319
|
+
try {
|
|
320
|
+
registerTasksCommand({
|
|
321
|
+
pi,
|
|
322
|
+
getNativeTaskStore: () => nativeTaskStore,
|
|
323
|
+
evaluateTaskBacklog,
|
|
324
|
+
updateWidget: () => {
|
|
325
|
+
widget.update();
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
registerNativeTaskTools({
|
|
329
|
+
pi,
|
|
330
|
+
taskStore,
|
|
331
|
+
evaluateTaskBacklog,
|
|
332
|
+
updateWidget: () => {
|
|
333
|
+
widget.update();
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
if (isStaleExtensionContextError(error)) {
|
|
339
|
+
debug("native task fallback skipped: extension context went stale");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
270
344
|
nativeTasksRegistered = true;
|
|
271
|
-
const taskStore = nativeTaskStore;
|
|
272
|
-
registerTasksCommand({
|
|
273
|
-
pi,
|
|
274
|
-
getNativeTaskStore: () => nativeTaskStore,
|
|
275
|
-
evaluateTaskBacklog,
|
|
276
|
-
updateWidget: () => {
|
|
277
|
-
widget.update();
|
|
278
|
-
},
|
|
279
|
-
});
|
|
280
|
-
registerNativeTaskTools({
|
|
281
|
-
pi,
|
|
282
|
-
taskStore,
|
|
283
|
-
evaluateTaskBacklog,
|
|
284
|
-
updateWidget: () => {
|
|
285
|
-
widget.update();
|
|
286
|
-
},
|
|
287
|
-
});
|
|
288
345
|
debug("native task tools registered (pi-tasks not detected)");
|
|
289
346
|
}, 6000);
|
|
347
|
+
pi.on("session_shutdown", () => {
|
|
348
|
+
clearTimeout(nativeTaskFallbackTimer);
|
|
349
|
+
});
|
|
290
350
|
}
|
package/dist/loop-parse.js
CHANGED
|
@@ -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) {
|
|
@@ -55,8 +57,8 @@ export function parseInterval(input) {
|
|
|
55
57
|
}
|
|
56
58
|
const match = trimmed.match(/^(\d+)\s*(s|m|h|d)$/i);
|
|
57
59
|
if (match) {
|
|
58
|
-
const value = parseInt(match[1], 10);
|
|
59
|
-
const unit = match[2].toLowerCase();
|
|
60
|
+
const value = parseInt(match[1] ?? "", 10);
|
|
61
|
+
const unit = (match[2] ?? "").toLowerCase();
|
|
60
62
|
const totalSec = value * (UNIT_TO_CRON[unit] ?? 60);
|
|
61
63
|
if (totalSec < 60) {
|
|
62
64
|
return { cron: `*/1 * * * *`, description: `${totalSec} seconds (rounded to 1 minute)` };
|
|
@@ -70,6 +72,13 @@ export function cronToNextFire(cronExpr, fromDate = new Date()) {
|
|
|
70
72
|
if (parts.length !== 5)
|
|
71
73
|
throw new Error(`Invalid cron expression: ${cronExpr}`);
|
|
72
74
|
const [minF, hourF, dayF, monthF, dowF] = parts;
|
|
75
|
+
if (minF === undefined ||
|
|
76
|
+
hourF === undefined ||
|
|
77
|
+
dayF === undefined ||
|
|
78
|
+
monthF === undefined ||
|
|
79
|
+
dowF === undefined) {
|
|
80
|
+
throw new Error(`Invalid cron expression: ${cronExpr}`);
|
|
81
|
+
}
|
|
73
82
|
const now = new Date(fromDate);
|
|
74
83
|
now.setSeconds(0, 0);
|
|
75
84
|
for (let minutesAdvanced = 1; minutesAdvanced < 525600; minutesAdvanced++) {
|
|
@@ -96,7 +105,7 @@ function cronFieldMatches(field, value) {
|
|
|
96
105
|
if (part === "*")
|
|
97
106
|
return true;
|
|
98
107
|
if (part.includes("/")) {
|
|
99
|
-
const [range, stepStr] = part.split("/");
|
|
108
|
+
const [range = "", stepStr = ""] = part.split("/");
|
|
100
109
|
const step = parseInt(stepStr, 10);
|
|
101
110
|
let rangeMin;
|
|
102
111
|
let rangeMax;
|
|
@@ -105,7 +114,7 @@ function cronFieldMatches(field, value) {
|
|
|
105
114
|
rangeMax = 59;
|
|
106
115
|
}
|
|
107
116
|
else if (range.includes("-")) {
|
|
108
|
-
const [minS, maxS] = range.split("-");
|
|
117
|
+
const [minS = "", maxS = ""] = range.split("-");
|
|
109
118
|
rangeMin = parseInt(minS, 10);
|
|
110
119
|
rangeMax = parseInt(maxS, 10);
|
|
111
120
|
}
|
|
@@ -121,7 +130,7 @@ function cronFieldMatches(field, value) {
|
|
|
121
130
|
continue;
|
|
122
131
|
}
|
|
123
132
|
if (part.includes("-")) {
|
|
124
|
-
const [minS, maxS] = part.split("-");
|
|
133
|
+
const [minS = "", maxS = ""] = part.split("-");
|
|
125
134
|
const min = parseInt(minS, 10);
|
|
126
135
|
const max = parseInt(maxS, 10);
|
|
127
136
|
if (value >= min && value <= max)
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { type ChildProcess, type SpawnOptions } from "node:child_process";
|
|
1
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import type { MonitorEntry, MonitorProcess } from "./types.js";
|
|
4
|
+
export type SpawnFn = (command: string, args: string[], options: SpawnOptions) => ChildProcess;
|
|
3
5
|
export declare class MonitorManager {
|
|
4
6
|
private pi;
|
|
5
7
|
private processes;
|
|
6
8
|
private nextId;
|
|
7
9
|
private onChange;
|
|
8
|
-
|
|
10
|
+
private spawnFn;
|
|
11
|
+
constructor(pi: ExtensionAPI, spawnFn?: SpawnFn);
|
|
9
12
|
/**
|
|
10
13
|
* Register a callback fired when a monitor's status changes or it is pruned
|
|
11
14
|
* (e.g. autonomous completion/error/stop/prune with no tool call). Used to
|
package/dist/monitor-manager.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
2
|
import { reduceMonitorState, } from "./monitor-reducer.js";
|
|
3
3
|
export class MonitorManager {
|
|
4
4
|
pi;
|
|
5
5
|
processes = new Map();
|
|
6
6
|
nextId = 1;
|
|
7
7
|
onChange;
|
|
8
|
-
|
|
8
|
+
spawnFn;
|
|
9
|
+
constructor(pi, spawnFn) {
|
|
9
10
|
this.pi = pi;
|
|
11
|
+
this.spawnFn = spawnFn ?? ((cmd, args, opts) => nodeSpawn(cmd, args, opts));
|
|
10
12
|
}
|
|
11
13
|
/**
|
|
12
14
|
* Register a callback fired when a monitor's status changes or it is pruned
|
|
@@ -76,7 +78,7 @@ export class MonitorManager {
|
|
|
76
78
|
const id = String(this.nextId - 1);
|
|
77
79
|
const entry = result.state.monitorsById[id];
|
|
78
80
|
const abortController = new AbortController();
|
|
79
|
-
const child =
|
|
81
|
+
const child = this.spawnFn("sh", ["-c", command], {
|
|
80
82
|
stdio: ["ignore", "pipe", "pipe"],
|
|
81
83
|
signal: abortController.signal,
|
|
82
84
|
env: { ...process.env },
|
|
@@ -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
|
|
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
|
|
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;
|