@trevonistrevon/pi-loop 0.5.6 → 0.5.8
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/coverage/coverage-final.json +27 -27
- package/coverage/index.html +39 -39
- package/coverage/src/commands/index.html +1 -1
- package/coverage/src/commands/loop-command.ts.html +3 -3
- package/coverage/src/commands/tasks-command.ts.html +3 -3
- package/coverage/src/coordinator.ts.html +20 -20
- package/coverage/src/goal-coordinator.ts.html +1 -1
- package/coverage/src/goal-reducer.ts.html +1 -1
- package/coverage/src/goal-store.ts.html +1 -1
- package/coverage/src/goal-verifier.ts.html +1 -1
- package/coverage/src/index.html +33 -33
- package/coverage/src/index.ts.html +71 -71
- package/coverage/src/loop-parse.ts.html +62 -62
- package/coverage/src/loop-reducer.ts.html +28 -28
- package/coverage/src/monitor-completion-coordinator.ts.html +3 -3
- package/coverage/src/monitor-manager.ts.html +76 -70
- package/coverage/src/monitor-reducer.ts.html +28 -28
- package/coverage/src/notification-reducer.ts.html +15 -15
- package/coverage/src/reducer-backed-store.ts.html +54 -54
- package/coverage/src/runtime/index.html +53 -53
- package/coverage/src/runtime/monitor-ondone-runtime.ts.html +39 -39
- package/coverage/src/runtime/notification-runtime.ts.html +42 -42
- package/coverage/src/runtime/scope.ts.html +34 -34
- package/coverage/src/runtime/session-runtime.ts.html +141 -51
- package/coverage/src/runtime/task-backlog-runtime.ts.html +48 -48
- package/coverage/src/runtime/task-rpc.ts.html +81 -81
- package/coverage/src/scheduler.ts.html +41 -41
- package/coverage/src/store.ts.html +26 -26
- package/coverage/src/task-backlog-coordinator.ts.html +8 -8
- package/coverage/src/task-reducer.ts.html +39 -39
- package/coverage/src/task-store.ts.html +55 -55
- package/coverage/src/tools/index.html +43 -43
- package/coverage/src/tools/loop-tools.ts.html +93 -93
- package/coverage/src/tools/monitor-tools.ts.html +53 -53
- package/coverage/src/tools/native-task-tools.ts.html +66 -66
- package/coverage/src/trigger-system.ts.html +11 -11
- package/coverage/src/ui/index.html +1 -1
- package/coverage/src/ui/widget.ts.html +22 -22
- package/dist/commands/tasks-command.js +12 -9
- package/dist/monitor-manager.js +3 -1
- package/dist/runtime/session-runtime.js +29 -0
- package/dist/runtime/task-events.d.ts +15 -0
- package/dist/runtime/task-events.js +13 -0
- package/dist/runtime/task-rpc.js +2 -0
- package/dist/tools/native-task-tools.js +20 -9
- package/package.json +1 -1
- package/src/commands/tasks-command.ts +9 -9
- package/src/monitor-manager.ts +3 -1
- package/src/runtime/session-runtime.ts +30 -0
- package/src/runtime/task-events.ts +41 -0
- package/src/runtime/task-rpc.ts +2 -0
- package/src/tools/native-task-tools.ts +16 -9
- package/vitest.config.ts +7 -7
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
|
+
import { emitNativeTaskEvent } from "../runtime/task-events.js";
|
|
2
3
|
function textResult(msg) {
|
|
3
4
|
return { content: [{ type: "text", text: msg }], details: undefined };
|
|
4
5
|
}
|
|
@@ -30,12 +31,7 @@ Fields:
|
|
|
30
31
|
}),
|
|
31
32
|
async execute(_toolCallId, params) {
|
|
32
33
|
const entry = taskStore.create(params.subject, params.description);
|
|
33
|
-
pi
|
|
34
|
-
taskId: entry.id,
|
|
35
|
-
subject: entry.subject,
|
|
36
|
-
description: entry.description,
|
|
37
|
-
status: entry.status,
|
|
38
|
-
});
|
|
34
|
+
emitNativeTaskEvent(pi, "tasks:created", entry);
|
|
39
35
|
const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
40
36
|
updateWidget();
|
|
41
37
|
const autoLoopMsg = backlog.created && backlog.entry
|
|
@@ -91,16 +87,28 @@ Parameters: id (required), status, subject, description`,
|
|
|
91
87
|
let entry = taskStore.get(id);
|
|
92
88
|
if (!entry)
|
|
93
89
|
return Promise.resolve(textResult(`Task #${id} not found`));
|
|
94
|
-
|
|
90
|
+
const previousStatus = entry.status;
|
|
91
|
+
if (status === "in_progress") {
|
|
95
92
|
entry = taskStore.start(id);
|
|
96
|
-
|
|
93
|
+
if (entry)
|
|
94
|
+
emitNativeTaskEvent(pi, "tasks:started", entry, previousStatus);
|
|
95
|
+
}
|
|
96
|
+
else if (status === "completed") {
|
|
97
97
|
entry = taskStore.complete(id);
|
|
98
|
-
|
|
98
|
+
if (entry)
|
|
99
|
+
emitNativeTaskEvent(pi, "tasks:completed", entry, previousStatus);
|
|
100
|
+
}
|
|
101
|
+
else if (status === "pending") {
|
|
99
102
|
entry = taskStore.reopen(id);
|
|
103
|
+
if (entry)
|
|
104
|
+
emitNativeTaskEvent(pi, "tasks:reopened", entry, previousStatus);
|
|
105
|
+
}
|
|
100
106
|
if (!entry)
|
|
101
107
|
return Promise.resolve(textResult(`Task #${id} not found`));
|
|
102
108
|
if (subject !== undefined || description !== undefined) {
|
|
103
109
|
entry = taskStore.updateDetails(id, { subject, description });
|
|
110
|
+
if (entry)
|
|
111
|
+
emitNativeTaskEvent(pi, "tasks:updated", entry, previousStatus);
|
|
104
112
|
}
|
|
105
113
|
if (!entry)
|
|
106
114
|
return Promise.resolve(textResult(`Task #${id} not found`));
|
|
@@ -121,9 +129,12 @@ Parameters: id (required), status, subject, description`,
|
|
|
121
129
|
id: Type.String({ description: "Task ID to delete" }),
|
|
122
130
|
}),
|
|
123
131
|
async execute(_toolCallId, params) {
|
|
132
|
+
const existing = taskStore.get(params.id);
|
|
124
133
|
const deleted = taskStore.delete(params.id);
|
|
125
134
|
updateWidget();
|
|
126
135
|
if (deleted) {
|
|
136
|
+
if (existing)
|
|
137
|
+
emitNativeTaskEvent(pi, "tasks:deleted", existing, existing.status);
|
|
127
138
|
await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
128
139
|
return Promise.resolve(textResult(`Task #${params.id} deleted`));
|
|
129
140
|
}
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { emitNativeTaskEvent } from "../runtime/task-events.js";
|
|
2
3
|
import { TaskStore } from "../task-store.js";
|
|
3
4
|
import type { TaskEntry } from "../task-types.js";
|
|
4
5
|
|
|
@@ -18,12 +19,7 @@ export function registerTasksCommand(options: TasksCommandOptions): void {
|
|
|
18
19
|
const { pi, getNativeTaskStore, evaluateTaskBacklog, updateWidget } = options;
|
|
19
20
|
|
|
20
21
|
async function emitCreated(entry: TaskEntry) {
|
|
21
|
-
pi
|
|
22
|
-
taskId: entry.id,
|
|
23
|
-
subject: entry.subject,
|
|
24
|
-
description: entry.description,
|
|
25
|
-
status: entry.status,
|
|
26
|
-
});
|
|
22
|
+
emitNativeTaskEvent(pi, "tasks:created", entry);
|
|
27
23
|
const taskStore = getNativeTaskStore();
|
|
28
24
|
if (!taskStore) return { created: false } satisfies TaskBacklogResult;
|
|
29
25
|
const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
@@ -94,15 +90,19 @@ export function registerTasksCommand(options: TasksCommandOptions): void {
|
|
|
94
90
|
|
|
95
91
|
if (action === "x Delete") {
|
|
96
92
|
taskStore.delete(task.id);
|
|
93
|
+
emitNativeTaskEvent(pi, "tasks:deleted", task, task.status);
|
|
97
94
|
ui.notify(`Task #${task.id} deleted`, "info");
|
|
98
95
|
} else if (action === "> Start") {
|
|
99
|
-
taskStore.start(task.id);
|
|
96
|
+
const next = taskStore.start(task.id);
|
|
97
|
+
if (next) emitNativeTaskEvent(pi, "tasks:started", next, task.status);
|
|
100
98
|
ui.notify(`Task #${task.id} started`, "info");
|
|
101
99
|
} else if (action === "ok Complete") {
|
|
102
|
-
taskStore.complete(task.id);
|
|
100
|
+
const next = taskStore.complete(task.id);
|
|
101
|
+
if (next) emitNativeTaskEvent(pi, "tasks:completed", next, task.status);
|
|
103
102
|
ui.notify(`Task #${task.id} completed`, "info");
|
|
104
103
|
} else if (action === "* Return to pending" || action === "* Reopen") {
|
|
105
|
-
taskStore.reopen(task.id);
|
|
104
|
+
const next = taskStore.reopen(task.id);
|
|
105
|
+
if (next) emitNativeTaskEvent(pi, "tasks:reopened", next, task.status);
|
|
106
106
|
ui.notify(`Task #${task.id} reopened`, "info");
|
|
107
107
|
}
|
|
108
108
|
|
package/src/monitor-manager.ts
CHANGED
|
@@ -58,7 +58,8 @@ export class MonitorManager {
|
|
|
58
58
|
// finish() and stop() so every terminal status is pruned consistently — a
|
|
59
59
|
// stopped monitor that is never pruned lingers in list() and the widget count.
|
|
60
60
|
private schedulePrune(id: string): void {
|
|
61
|
-
|
|
61
|
+
// unref so a pending prune never keeps a one-shot (`pi -p`) process alive.
|
|
62
|
+
const timer = setTimeout(() => {
|
|
62
63
|
this.applyReducerEvent({
|
|
63
64
|
type: "MONITOR_PRUNED",
|
|
64
65
|
at: Date.now(),
|
|
@@ -68,6 +69,7 @@ export class MonitorManager {
|
|
|
68
69
|
payload: { id },
|
|
69
70
|
});
|
|
70
71
|
}, 30000);
|
|
72
|
+
timer.unref?.();
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
create(command: string, description?: string, timeout = 300000): MonitorEntry {
|
|
@@ -7,6 +7,10 @@ export interface SessionSwitchEvent {
|
|
|
7
7
|
reason?: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
// Wall-clock cadence for the idle heartbeat that pumps the scheduler. Cron is
|
|
11
|
+
// minute-granular, so 30s gives sub-minute wake latency while idle.
|
|
12
|
+
const HEARTBEAT_MS = 30_000;
|
|
13
|
+
|
|
10
14
|
export interface SessionRuntimeOptions {
|
|
11
15
|
pi: ExtensionAPI;
|
|
12
16
|
getLoopScope: () => LoopScope;
|
|
@@ -48,6 +52,27 @@ export function registerSessionRuntimeHooks(options: SessionRuntimeOptions): voi
|
|
|
48
52
|
|
|
49
53
|
let storeUpgraded = false;
|
|
50
54
|
let persistedShown = false;
|
|
55
|
+
let heartbeatTimer: ReturnType<typeof setInterval> | undefined;
|
|
56
|
+
|
|
57
|
+
// The CronScheduler is pump-driven; without this heartbeat it only advances at
|
|
58
|
+
// turn boundaries (turn_start/agent_end), so a loop whose fire time elapses
|
|
59
|
+
// while the agent is idle would never fire and never re-wake the agent. The
|
|
60
|
+
// timer is unref'd so it never keeps a one-shot (`pi -p`) process alive.
|
|
61
|
+
function ensureHeartbeat(): void {
|
|
62
|
+
if (heartbeatTimer) return;
|
|
63
|
+
heartbeatTimer = setInterval(() => {
|
|
64
|
+
// Swallow pump failures so a transient error never surfaces as an
|
|
65
|
+
// unhandled rejection; the next tick retries.
|
|
66
|
+
void pumpLoops().catch(() => {});
|
|
67
|
+
}, HEARTBEAT_MS);
|
|
68
|
+
heartbeatTimer.unref?.();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function stopHeartbeat(): void {
|
|
72
|
+
if (!heartbeatTimer) return;
|
|
73
|
+
clearInterval(heartbeatTimer);
|
|
74
|
+
heartbeatTimer = undefined;
|
|
75
|
+
}
|
|
51
76
|
|
|
52
77
|
function upgradeStoreIfNeeded(ctx: ExtensionContext) {
|
|
53
78
|
if (storeUpgraded) return;
|
|
@@ -66,6 +91,7 @@ export function registerSessionRuntimeHooks(options: SessionRuntimeOptions): voi
|
|
|
66
91
|
getStore().clearExpired();
|
|
67
92
|
getStore().expireEventLoops(sessionStartedAt);
|
|
68
93
|
getTriggerSystem().start();
|
|
94
|
+
ensureHeartbeat();
|
|
69
95
|
widget.update();
|
|
70
96
|
}
|
|
71
97
|
}
|
|
@@ -89,6 +115,7 @@ export function registerSessionRuntimeHooks(options: SessionRuntimeOptions): voi
|
|
|
89
115
|
setSessionId(ctx.sessionManager.getSessionId());
|
|
90
116
|
widget.setUICtx(ctx.ui);
|
|
91
117
|
upgradeStoreIfNeeded(ctx);
|
|
118
|
+
ensureHeartbeat();
|
|
92
119
|
widget.update();
|
|
93
120
|
await pumpLoops();
|
|
94
121
|
});
|
|
@@ -97,6 +124,7 @@ export function registerSessionRuntimeHooks(options: SessionRuntimeOptions): voi
|
|
|
97
124
|
setLatestCtx(ctx);
|
|
98
125
|
widget.setUICtx(ctx.ui);
|
|
99
126
|
upgradeStoreIfNeeded(ctx);
|
|
127
|
+
ensureHeartbeat();
|
|
100
128
|
showPersistedLoops();
|
|
101
129
|
widget.update();
|
|
102
130
|
});
|
|
@@ -123,6 +151,7 @@ export function registerSessionRuntimeHooks(options: SessionRuntimeOptions): voi
|
|
|
123
151
|
});
|
|
124
152
|
|
|
125
153
|
pi.on("session_shutdown", async () => {
|
|
154
|
+
stopHeartbeat();
|
|
126
155
|
notificationRuntime.clear("session_shutdown");
|
|
127
156
|
});
|
|
128
157
|
|
|
@@ -130,6 +159,7 @@ export function registerSessionRuntimeHooks(options: SessionRuntimeOptions): voi
|
|
|
130
159
|
setLatestCtx(ctx);
|
|
131
160
|
widget.setUICtx(ctx.ui);
|
|
132
161
|
getTriggerSystem().stop();
|
|
162
|
+
stopHeartbeat();
|
|
133
163
|
notificationRuntime.clear("session_switch");
|
|
134
164
|
setSessionId(undefined);
|
|
135
165
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { TaskEntry, TaskStatus } from "../task-types.js";
|
|
3
|
+
|
|
4
|
+
export type NativeTaskEventName =
|
|
5
|
+
| "tasks:created"
|
|
6
|
+
| "tasks:started"
|
|
7
|
+
| "tasks:completed"
|
|
8
|
+
| "tasks:reopened"
|
|
9
|
+
| "tasks:updated"
|
|
10
|
+
| "tasks:deleted";
|
|
11
|
+
|
|
12
|
+
export interface NativeTaskEventPayload {
|
|
13
|
+
taskId: string;
|
|
14
|
+
subject: string;
|
|
15
|
+
description: string;
|
|
16
|
+
status: TaskStatus;
|
|
17
|
+
previousStatus?: TaskStatus;
|
|
18
|
+
createdAt: number;
|
|
19
|
+
updatedAt: number;
|
|
20
|
+
completedAt?: number;
|
|
21
|
+
metadata?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function emitNativeTaskEvent(
|
|
25
|
+
pi: ExtensionAPI,
|
|
26
|
+
name: NativeTaskEventName,
|
|
27
|
+
entry: TaskEntry,
|
|
28
|
+
previousStatus?: TaskStatus,
|
|
29
|
+
): void {
|
|
30
|
+
pi.events.emit(name, {
|
|
31
|
+
taskId: entry.id,
|
|
32
|
+
subject: entry.subject,
|
|
33
|
+
description: entry.description,
|
|
34
|
+
status: entry.status,
|
|
35
|
+
previousStatus,
|
|
36
|
+
createdAt: entry.createdAt,
|
|
37
|
+
updatedAt: entry.updatedAt,
|
|
38
|
+
completedAt: entry.completedAt,
|
|
39
|
+
metadata: entry.metadata,
|
|
40
|
+
} satisfies NativeTaskEventPayload);
|
|
41
|
+
}
|
package/src/runtime/task-rpc.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { TaskStore } from "../task-store.js";
|
|
4
4
|
import type { LoopEntry } from "../types.js";
|
|
5
|
+
import { emitNativeTaskEvent } from "./task-events.js";
|
|
5
6
|
|
|
6
7
|
export interface TaskRuntimeBridgeOptions {
|
|
7
8
|
pi: ExtensionAPI;
|
|
@@ -82,6 +83,7 @@ export function createTaskRuntimeBridge(options: TaskRuntimeBridgeOptions): Task
|
|
|
82
83
|
`Auto-created from loop #${entry.id}`,
|
|
83
84
|
{ loopId: entry.id, trigger: entry.trigger },
|
|
84
85
|
);
|
|
86
|
+
emitNativeTaskEvent(pi, "tasks:created", task);
|
|
85
87
|
onNativeTaskCreated?.(nativeTaskStore);
|
|
86
88
|
return task.id;
|
|
87
89
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
+
import { emitNativeTaskEvent } from "../runtime/task-events.js";
|
|
3
4
|
import { TaskStore } from "../task-store.js";
|
|
4
5
|
|
|
5
6
|
export interface TaskBacklogResult {
|
|
@@ -47,12 +48,7 @@ Fields:
|
|
|
47
48
|
}),
|
|
48
49
|
async execute(_toolCallId, params) {
|
|
49
50
|
const entry = taskStore.create(params.subject, params.description);
|
|
50
|
-
pi
|
|
51
|
-
taskId: entry.id,
|
|
52
|
-
subject: entry.subject,
|
|
53
|
-
description: entry.description,
|
|
54
|
-
status: entry.status,
|
|
55
|
-
});
|
|
51
|
+
emitNativeTaskEvent(pi, "tasks:created", entry);
|
|
56
52
|
const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
57
53
|
updateWidget();
|
|
58
54
|
|
|
@@ -111,13 +107,22 @@ Parameters: id (required), status, subject, description`,
|
|
|
111
107
|
let entry = taskStore.get(id);
|
|
112
108
|
if (!entry) return Promise.resolve(textResult(`Task #${id} not found`));
|
|
113
109
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
110
|
+
const previousStatus = entry.status;
|
|
111
|
+
if (status === "in_progress") {
|
|
112
|
+
entry = taskStore.start(id);
|
|
113
|
+
if (entry) emitNativeTaskEvent(pi, "tasks:started", entry, previousStatus);
|
|
114
|
+
} else if (status === "completed") {
|
|
115
|
+
entry = taskStore.complete(id);
|
|
116
|
+
if (entry) emitNativeTaskEvent(pi, "tasks:completed", entry, previousStatus);
|
|
117
|
+
} else if (status === "pending") {
|
|
118
|
+
entry = taskStore.reopen(id);
|
|
119
|
+
if (entry) emitNativeTaskEvent(pi, "tasks:reopened", entry, previousStatus);
|
|
120
|
+
}
|
|
117
121
|
|
|
118
122
|
if (!entry) return Promise.resolve(textResult(`Task #${id} not found`));
|
|
119
123
|
if (subject !== undefined || description !== undefined) {
|
|
120
124
|
entry = taskStore.updateDetails(id, { subject, description });
|
|
125
|
+
if (entry) emitNativeTaskEvent(pi, "tasks:updated", entry, previousStatus);
|
|
121
126
|
}
|
|
122
127
|
if (!entry) return Promise.resolve(textResult(`Task #${id} not found`));
|
|
123
128
|
updateWidget();
|
|
@@ -138,9 +143,11 @@ Parameters: id (required), status, subject, description`,
|
|
|
138
143
|
id: Type.String({ description: "Task ID to delete" }),
|
|
139
144
|
}),
|
|
140
145
|
async execute(_toolCallId, params) {
|
|
146
|
+
const existing = taskStore.get(params.id);
|
|
141
147
|
const deleted = taskStore.delete(params.id);
|
|
142
148
|
updateWidget();
|
|
143
149
|
if (deleted) {
|
|
150
|
+
if (existing) emitNativeTaskEvent(pi, "tasks:deleted", existing, existing.status);
|
|
144
151
|
await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
|
|
145
152
|
return Promise.resolve(textResult(`Task #${params.id} deleted`));
|
|
146
153
|
}
|
package/vitest.config.ts
CHANGED
|
@@ -11,14 +11,14 @@ export default defineConfig({
|
|
|
11
11
|
include: ["src/**/*.ts"],
|
|
12
12
|
// Type-only modules and test helpers carry no executable logic worth gating.
|
|
13
13
|
exclude: ["src/**/*-types.ts", "src/types.ts"],
|
|
14
|
-
// Floors set just below current actuals (stmts
|
|
15
|
-
// funcs
|
|
16
|
-
// runtime/
|
|
14
|
+
// Floors set just below current actuals (stmts 84%, branches 75%,
|
|
15
|
+
// funcs 95%, lines 86%) to catch regressions. Raised in Phase 4 after the
|
|
16
|
+
// runtime/ + tools/ suites landed.
|
|
17
17
|
thresholds: {
|
|
18
|
-
statements:
|
|
19
|
-
branches:
|
|
20
|
-
functions:
|
|
21
|
-
lines:
|
|
18
|
+
statements: 82,
|
|
19
|
+
branches: 73,
|
|
20
|
+
functions: 94,
|
|
21
|
+
lines: 84,
|
|
22
22
|
},
|
|
23
23
|
},
|
|
24
24
|
},
|