@llblab/pi-actors 0.49.1 → 0.49.2
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/BACKLOG.md +11 -1
- package/CHANGELOG.md +4 -0
- package/dist/lib/extension-runtime.js +29 -10
- package/dist/lib/observability.js +6 -1
- package/dist/lib/run-ui-runtime.d.ts +10 -3
- package/dist/lib/run-ui-runtime.js +109 -52
- package/dist/lib/runtime.d.ts +2 -0
- package/dist/lib/runtime.js +24 -4
- package/lib/extension-runtime.ts +26 -10
- package/lib/observability.ts +5 -1
- package/lib/run-ui-runtime.ts +128 -50
- package/lib/runtime.ts +26 -7
- package/package.json +1 -1
package/BACKLOG.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# Project Backlog
|
|
2
2
|
|
|
3
|
-
- [ ] `
|
|
3
|
+
- [ ] `0.50.0 hardening`: Close the confirmed authorization, bounded-inspection, and operator-truth gaps from the v0.49.1 production-readiness review.
|
|
4
|
+
- [ ] `Run boundary checkpoint`: Public Run operations remain owner-safe and bounded under missing identity and adversarial evidence sizes.
|
|
5
|
+
- [ ] Make Run-specific `inspect` and `message` operations fail closed when the caller session identity is unavailable, keep runtime inventory owner-filtered, and add missing-context plus cross-owner regressions.
|
|
6
|
+
- [ ] Bound session-evidence and artifact-manifest inspection before full-file materialization; use capped/streaming reads or hashing as appropriate and add adversarial-size regressions.
|
|
7
|
+
- [ ] `Operator truth checkpoint`: Public status and recovery guidance exactly match supported runtime behavior.
|
|
8
|
+
- [ ] Route automatic-review status through the canonical policy parser so `0`, `false`, and `off` are reported consistently without case sensitivity.
|
|
9
|
+
- [ ] Replace the removed `session:<id>` and `session:all` recovery hints with supported inspection guidance.
|
|
10
|
+
- [ ] Document exact `message target=run:<id>` examples for runtime-owned `kill`, `archive`, and `prune`, including their state and artifact-preservation constraints.
|
|
11
|
+
- [ ] Pass focused boundary and operator-contract regressions plus full package validation before release.
|
|
12
|
+
|
|
13
|
+
- [ ] `Future minor — Linux MPRIS media integration`: Expose the active `music-player/playback` singleton as one optional generation-fenced MPRIS2 player so GNOME and compatible desktop shells can show current media and native controls without making D-Bus a second playback authority; keep this feature outside the 0.49.2 and 0.50.0 cohorts.
|
|
4
14
|
- [ ] Publish `PlaybackStatus`, track metadata, duration, read-time position, volume, and supported capabilities under one stable session-scoped bus identity; disappear cleanly when the Run stops or its generation is replaced, and fail soft when the user D-Bus session is unavailable.
|
|
5
15
|
- [ ] Map `Play`, `Pause`, `PlayPause`, `Next`, `Previous`, `Stop`, `Seek`, `SetPosition`, and `Volume` back into the existing generation-fenced music-player Control/helper contract rather than signaling the backend or editing Run state directly.
|
|
6
16
|
- [ ] Validate deterministic D-Bus contract behavior plus a live GNOME smoke showing the media surface, metadata, progress, volume, and controls while preserving backend independence and exact Actor ownership.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.49.2: Stale-Context Lifecycle Hotfix
|
|
8
|
+
|
|
9
|
+
- `Stale-Context Lifecycle Hotfix`: Contains Run UI animation, delayed watcher, reconciliation, error-handler, retirement, and Recipe-reload callbacks so invalidated Pi contexts cannot escape into the host event loop. Session owners are captured while live, stale shutdown cannot close a replacement session, and parent teardown plus shutdown notification remain identity-fenced and no-throw (GitHub issue #125).
|
|
10
|
+
|
|
7
11
|
## 0.49.1: Maintained Telegram View Routing
|
|
8
12
|
|
|
9
13
|
- `Maintained Telegram View Routing`: Music Player now treats Telegram-originated control intent as a breadcrumb to its ready capability-owned Generative App, preferring bind/invoke over one-shot prompt buttons while preserving Actor playback authority and an explicit model-mediated fallback when the app runtime is unavailable.
|
|
@@ -17,6 +17,8 @@ import * as Tools from "./tools.js";
|
|
|
17
17
|
import * as ToolsResponse from "./tools-response.js";
|
|
18
18
|
export function createActorExtensionRuntime(pi) {
|
|
19
19
|
let activeRunContext;
|
|
20
|
+
let activeRunOwnerId;
|
|
21
|
+
const runOwnerIdsByContext = new WeakMap();
|
|
20
22
|
const recipeResolutionContextsBySession = new Map();
|
|
21
23
|
const getRunOwnerId = Pi.getSessionId;
|
|
22
24
|
const getRecipeResolutionContext = (ctx) => {
|
|
@@ -32,9 +34,21 @@ export function createActorExtensionRuntime(pi) {
|
|
|
32
34
|
getRunOwnerId,
|
|
33
35
|
getThinkingLevel: () => pi.getThinkingLevel(),
|
|
34
36
|
});
|
|
35
|
-
|
|
37
|
+
let recipeReload;
|
|
38
|
+
let runUiRuntime;
|
|
39
|
+
const closeActiveSessionRuntimes = () => {
|
|
40
|
+
const ownerId = activeRunOwnerId;
|
|
41
|
+
activeRunContext = undefined;
|
|
42
|
+
activeRunOwnerId = undefined;
|
|
43
|
+
runUiRuntime?.close();
|
|
44
|
+
automaticReview.close();
|
|
45
|
+
recipeReload?.close();
|
|
46
|
+
if (ownerId)
|
|
47
|
+
recipeResolutionContextsBySession.delete(ownerId);
|
|
48
|
+
};
|
|
49
|
+
runUiRuntime = RunUiRuntime.createRunUiRuntime({
|
|
36
50
|
getActiveContext: () => activeRunContext,
|
|
37
|
-
|
|
51
|
+
onCallbackError: closeActiveSessionRuntimes,
|
|
38
52
|
onRunEvent: automaticReview.schedule,
|
|
39
53
|
pi,
|
|
40
54
|
});
|
|
@@ -77,10 +91,11 @@ export function createActorExtensionRuntime(pi) {
|
|
|
77
91
|
reservedToolNames: Tools.RESERVED_TOOL_NAMES,
|
|
78
92
|
setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
|
|
79
93
|
});
|
|
80
|
-
|
|
94
|
+
recipeReload = Runtime.createRecipeToolReloadWatcher(runtime, {
|
|
81
95
|
getResolutionContext: () => activeRunContext
|
|
82
96
|
? getRecipeResolutionContext(activeRunContext)
|
|
83
97
|
: undefined,
|
|
98
|
+
onCallbackError: closeActiveSessionRuntimes,
|
|
84
99
|
});
|
|
85
100
|
return {
|
|
86
101
|
beforeAgentStart(systemPrompt, skills, ctx) {
|
|
@@ -102,25 +117,29 @@ export function createActorExtensionRuntime(pi) {
|
|
|
102
117
|
automaticReview.schedule();
|
|
103
118
|
},
|
|
104
119
|
onSessionShutdown(reason, ctx) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
120
|
+
const ownerId = runOwnerIdsByContext.get(ctx);
|
|
121
|
+
runOwnerIdsByContext.delete(ctx);
|
|
122
|
+
if (activeRunContext === ctx)
|
|
123
|
+
closeActiveSessionRuntimes();
|
|
124
|
+
if (ownerId)
|
|
125
|
+
recipeResolutionContextsBySession.delete(ownerId);
|
|
126
|
+
runUiRuntime.shutdown(reason, ownerId, ctx);
|
|
110
127
|
},
|
|
111
128
|
async onSessionStart(ctx) {
|
|
112
129
|
const sessionId = getRunOwnerId(ctx);
|
|
113
130
|
recipeResolutionContextsBySession.set(sessionId, RecipeResolution.createEmptyRecipeResolutionContext(sessionId, ctx.cwd));
|
|
114
131
|
ctx.ui.setWidget("zz-pi-actors-comms", undefined);
|
|
115
132
|
activeRunContext = ctx;
|
|
133
|
+
activeRunOwnerId = sessionId;
|
|
134
|
+
runOwnerIdsByContext.set(ctx, sessionId);
|
|
116
135
|
runUiRuntime.close();
|
|
117
136
|
automaticReview.close();
|
|
118
137
|
recipeReload.close();
|
|
119
138
|
await Temp.prepareExtensionTempDir(Paths.EXTENSION_RUNTIME_PATHS.tempDir);
|
|
120
|
-
if (activeRunContext !== ctx)
|
|
139
|
+
if (activeRunContext !== ctx || activeRunOwnerId !== sessionId)
|
|
121
140
|
return;
|
|
122
141
|
automaticReview.start(ctx);
|
|
123
|
-
runUiRuntime.start(ctx);
|
|
142
|
+
runUiRuntime.start(ctx, sessionId);
|
|
124
143
|
recipeReload.watch(ctx);
|
|
125
144
|
},
|
|
126
145
|
registerCoreTools() {
|
|
@@ -229,7 +229,12 @@ export function createRunTerminalReconciliationLoop(input) {
|
|
|
229
229
|
input.reconcile();
|
|
230
230
|
}
|
|
231
231
|
catch (error) {
|
|
232
|
-
|
|
232
|
+
try {
|
|
233
|
+
input.onError?.(error);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
/* reconciliation callbacks must never escape into the host event loop */
|
|
237
|
+
}
|
|
233
238
|
}
|
|
234
239
|
};
|
|
235
240
|
const close = () => {
|
|
@@ -3,16 +3,23 @@
|
|
|
3
3
|
* Zones: run watcher lifecycle, terminal reconciliation, status animation, shutdown teardown
|
|
4
4
|
* Owns event-driven run UI coordination without owning actor execution semantics.
|
|
5
5
|
*/
|
|
6
|
+
import * as AsyncRuns from "./async-runs.ts";
|
|
7
|
+
import * as Observability from "./observability.ts";
|
|
6
8
|
import * as Pi from "./pi.ts";
|
|
7
9
|
export interface RunUiRuntime {
|
|
8
10
|
close(): void;
|
|
9
|
-
shutdown(eventReason: string,
|
|
10
|
-
start(ctx: Pi.ExtensionContext): void;
|
|
11
|
+
shutdown(eventReason: string, ownerId: string | undefined, ctx?: Pi.ExtensionContext): void;
|
|
12
|
+
start(ctx: Pi.ExtensionContext, ownerId: string): void;
|
|
11
13
|
}
|
|
12
14
|
export interface RunUiRuntimeDeps {
|
|
15
|
+
animationIntervalMs?: number;
|
|
16
|
+
createRunStateWatcher?: typeof Observability.createRunStateWatcher;
|
|
17
|
+
createRunTerminalReconciliationLoop?: typeof Observability.createRunTerminalReconciliationLoop;
|
|
13
18
|
getActiveContext(): Pi.ExtensionContext | undefined;
|
|
14
|
-
|
|
19
|
+
notificationDelayMs?: number;
|
|
20
|
+
onCallbackError?: (error: unknown) => void;
|
|
15
21
|
onRunEvent(): void;
|
|
16
22
|
pi: Pi.ExtensionAPI;
|
|
23
|
+
teardownRunsOwnedByParent?: typeof AsyncRuns.teardownRunsOwnedByParent;
|
|
17
24
|
}
|
|
18
25
|
export declare function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime;
|
|
@@ -8,22 +8,78 @@ import * as Observability from "./observability.js";
|
|
|
8
8
|
import * as Paths from "./paths.js";
|
|
9
9
|
import * as Pi from "./pi.js";
|
|
10
10
|
export function createRunUiRuntime(deps) {
|
|
11
|
+
let activeContext;
|
|
12
|
+
let activeOwnerId;
|
|
11
13
|
let animationInterval;
|
|
12
14
|
let notifyTimeout;
|
|
15
|
+
let running = false;
|
|
13
16
|
let lastWatcherDiagnosticId = 0;
|
|
14
17
|
const observation = Observability.createRunUiObservationState();
|
|
15
18
|
const retirementAttempts = new Set();
|
|
16
19
|
const terminalNotificationsInFlight = new Set();
|
|
20
|
+
const close = () => {
|
|
21
|
+
running = false;
|
|
22
|
+
activeContext = undefined;
|
|
23
|
+
activeOwnerId = undefined;
|
|
24
|
+
try {
|
|
25
|
+
watcher.close();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
/* cleanup must not escape a host callback */
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
reconciliation.close();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
/* cleanup must not escape a host callback */
|
|
35
|
+
}
|
|
36
|
+
if (notifyTimeout)
|
|
37
|
+
clearTimeout(notifyTimeout);
|
|
38
|
+
notifyTimeout = undefined;
|
|
39
|
+
if (animationInterval)
|
|
40
|
+
clearInterval(animationInterval);
|
|
41
|
+
animationInterval = undefined;
|
|
42
|
+
};
|
|
43
|
+
const stopAfterCallbackFailure = (label, error, expectedContext) => {
|
|
44
|
+
if (activeContext !== expectedContext)
|
|
45
|
+
return;
|
|
46
|
+
close();
|
|
47
|
+
try {
|
|
48
|
+
deps.onCallbackError?.(error);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* host callback containment must remain no-throw */
|
|
52
|
+
}
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
try {
|
|
55
|
+
expectedContext.ui.notify(`Actor ${label} failed: ${message}`, "error");
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* stale context or unavailable UI */
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const runActiveCallback = (label, callback) => {
|
|
62
|
+
if (!running || !activeContext || !activeOwnerId)
|
|
63
|
+
return;
|
|
64
|
+
const ctx = activeContext;
|
|
65
|
+
try {
|
|
66
|
+
if (deps.getActiveContext() !== ctx)
|
|
67
|
+
return;
|
|
68
|
+
callback(ctx, activeOwnerId);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
stopAfterCallbackFailure(label, error, ctx);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
17
74
|
const retireCandidateRuns = (ctx, summary) => {
|
|
18
75
|
void Observability.executeRunRetirements(summary, {
|
|
19
76
|
attempted: retirementAttempts,
|
|
20
77
|
cancelRun: (candidate) => AsyncRuns.cancelRun(candidate.stateDir),
|
|
21
78
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
22
79
|
sendStop: async (candidate) => AsyncRuns.cancelRun(candidate.stateDir),
|
|
23
|
-
});
|
|
80
|
+
}).catch((error) => stopAfterCallbackFailure("Run retirement callback", error, ctx));
|
|
24
81
|
};
|
|
25
|
-
const update = (ctx, notify = false, terminalOnly = false) => {
|
|
26
|
-
const ownerId = deps.getRunOwnerId(ctx);
|
|
82
|
+
const update = (ctx, ownerId, notify = false, terminalOnly = false) => {
|
|
27
83
|
const snapshot = Observability.readRunUiSnapshot(observation, ownerId);
|
|
28
84
|
ctx.ui.setStatus("zz-pi-actors-runs", snapshot.status ? ctx.ui.theme.fg("dim", snapshot.status) : undefined);
|
|
29
85
|
if (!notify)
|
|
@@ -45,62 +101,55 @@ export function createRunUiRuntime(deps) {
|
|
|
45
101
|
}
|
|
46
102
|
};
|
|
47
103
|
const scheduleUpdate = () => {
|
|
104
|
+
if (!running)
|
|
105
|
+
return;
|
|
48
106
|
if (notifyTimeout)
|
|
49
107
|
clearTimeout(notifyTimeout);
|
|
50
108
|
notifyTimeout = setTimeout(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}, 50);
|
|
109
|
+
runActiveCallback("Run watcher callback", (ctx, ownerId) => {
|
|
110
|
+
watcher.refresh();
|
|
111
|
+
update(ctx, ownerId, true);
|
|
112
|
+
deps.onRunEvent();
|
|
113
|
+
reportDiagnostics(ctx);
|
|
114
|
+
});
|
|
115
|
+
}, deps.notificationDelayMs ?? 50);
|
|
59
116
|
notifyTimeout.unref?.();
|
|
60
117
|
};
|
|
61
|
-
const watcher = Observability.createRunStateWatcher({
|
|
118
|
+
const watcher = (deps.createRunStateWatcher ?? Observability.createRunStateWatcher)({
|
|
62
119
|
stateRoot: Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
|
|
63
120
|
onChange: scheduleUpdate,
|
|
64
121
|
});
|
|
65
|
-
const reconciliation =
|
|
122
|
+
const reconciliation = (deps.createRunTerminalReconciliationLoop ??
|
|
123
|
+
Observability.createRunTerminalReconciliationLoop)({
|
|
66
124
|
onError: (error) => {
|
|
67
|
-
|
|
68
|
-
if (!ctx)
|
|
125
|
+
if (!running || !activeContext)
|
|
69
126
|
return;
|
|
70
|
-
|
|
71
|
-
ctx.ui.notify(`Actor terminal reconciliation failed: ${message}`, "error");
|
|
127
|
+
stopAfterCallbackFailure("terminal reconciliation callback", error, activeContext);
|
|
72
128
|
},
|
|
73
129
|
reconcile: () => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
130
|
+
runActiveCallback("terminal reconciliation callback", (ctx, ownerId) => {
|
|
131
|
+
Observability.reconcileRunTerminalNotifications({
|
|
132
|
+
inFlight: terminalNotificationsInFlight,
|
|
133
|
+
ownerId,
|
|
134
|
+
sink: Pi.createNotificationSink(deps.pi, ctx),
|
|
135
|
+
state: observation,
|
|
136
|
+
includeAttention: true,
|
|
137
|
+
});
|
|
138
|
+
reportDiagnostics(ctx);
|
|
83
139
|
});
|
|
84
|
-
reportDiagnostics(ctx);
|
|
85
140
|
},
|
|
86
|
-
refreshWatcher: () =>
|
|
141
|
+
refreshWatcher: () => {
|
|
142
|
+
if (running)
|
|
143
|
+
watcher.refresh();
|
|
144
|
+
},
|
|
87
145
|
});
|
|
88
|
-
const close = () => {
|
|
89
|
-
watcher.close();
|
|
90
|
-
reconciliation.close();
|
|
91
|
-
if (notifyTimeout)
|
|
92
|
-
clearTimeout(notifyTimeout);
|
|
93
|
-
notifyTimeout = undefined;
|
|
94
|
-
if (animationInterval)
|
|
95
|
-
clearInterval(animationInterval);
|
|
96
|
-
animationInterval = undefined;
|
|
97
|
-
};
|
|
98
146
|
return {
|
|
99
147
|
close,
|
|
100
|
-
shutdown(eventReason, ctx) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
148
|
+
shutdown(eventReason, ownerId, ctx) {
|
|
149
|
+
if (!ownerId)
|
|
150
|
+
return;
|
|
151
|
+
const teardown = (deps.teardownRunsOwnedByParent ?? AsyncRuns.teardownRunsOwnedByParent)(ownerId, Paths.EXTENSION_RUNTIME_PATHS.runStateRoot, { trigger: `session_shutdown:${eventReason}` });
|
|
152
|
+
if (teardown.failed === 0 || !ctx)
|
|
104
153
|
return;
|
|
105
154
|
try {
|
|
106
155
|
ctx.ui.notify(`Actor shutdown teardown: killed=${teardown.killed} failed=${teardown.failed} skipped=${teardown.skipped} discovery_failed=${teardown.discoveryFailed}. Summary: ${teardown.summaryPath ?? "unavailable"}.`, "warning");
|
|
@@ -109,17 +158,25 @@ export function createRunUiRuntime(deps) {
|
|
|
109
158
|
/* stale shutdown context */
|
|
110
159
|
}
|
|
111
160
|
},
|
|
112
|
-
start(ctx) {
|
|
161
|
+
start(ctx, ownerId) {
|
|
113
162
|
close();
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
163
|
+
activeContext = ctx;
|
|
164
|
+
activeOwnerId = ownerId;
|
|
165
|
+
running = true;
|
|
166
|
+
try {
|
|
167
|
+
Observability.primeRunAttentionState(observation, ownerId);
|
|
168
|
+
update(ctx, ownerId, true, true);
|
|
169
|
+
watcher.refresh();
|
|
170
|
+
reconciliation.start();
|
|
171
|
+
animationInterval = setInterval(() => {
|
|
172
|
+
runActiveCallback("status animation callback", (current, currentOwnerId) => update(current, currentOwnerId));
|
|
173
|
+
}, deps.animationIntervalMs ?? 1000);
|
|
174
|
+
animationInterval.unref?.();
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
close();
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
123
180
|
},
|
|
124
181
|
};
|
|
125
182
|
}
|
package/dist/lib/runtime.d.ts
CHANGED
|
@@ -63,7 +63,9 @@ export declare function createAutoToolsRuntime(deps: ToolRegistryRuntimeDeps): T
|
|
|
63
63
|
export interface RecipeToolReloadWatcherDeps {
|
|
64
64
|
exists?: (path: string) => boolean;
|
|
65
65
|
getResolutionContext?: () => RecipeResolutionContext | undefined;
|
|
66
|
+
onCallbackError?: (error: unknown) => void;
|
|
66
67
|
recipeRoot?: string;
|
|
68
|
+
reloadDelayMs?: number;
|
|
67
69
|
watchPath?: typeof watch;
|
|
68
70
|
}
|
|
69
71
|
export declare function createRecipeToolReloadWatcher(runtime: Pick<ToolRegistryRuntime, "loadTools"> & Partial<Pick<ToolRegistryRuntime, "setWatchStatus">>, deps?: RecipeToolReloadWatcherDeps): RecipeToolReloadWatcher;
|
package/dist/lib/runtime.js
CHANGED
|
@@ -236,21 +236,41 @@ export function createRecipeToolReloadWatcher(runtime, deps = {}) {
|
|
|
236
236
|
reloadTimeout = undefined;
|
|
237
237
|
setWatchStatus("closed");
|
|
238
238
|
};
|
|
239
|
+
const reportCallbackError = (error) => {
|
|
240
|
+
try {
|
|
241
|
+
deps.onCallbackError?.(error);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
/* host callback containment must remain no-throw */
|
|
245
|
+
}
|
|
246
|
+
};
|
|
239
247
|
const notifyFailure = (ctx) => {
|
|
240
248
|
if (failureNotified)
|
|
241
249
|
return;
|
|
242
250
|
failureNotified = true;
|
|
243
251
|
setWatchStatus("failed");
|
|
244
|
-
|
|
252
|
+
try {
|
|
253
|
+
ctx.ui.notify("Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.", "warning");
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
reportCallbackError(error);
|
|
257
|
+
}
|
|
245
258
|
};
|
|
246
259
|
const scheduleReload = (ctx) => {
|
|
247
260
|
failureNotified = false;
|
|
248
261
|
if (reloadTimeout)
|
|
249
262
|
clearTimeout(reloadTimeout);
|
|
250
263
|
reloadTimeout = setTimeout(() => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
264
|
+
reloadTimeout = undefined;
|
|
265
|
+
try {
|
|
266
|
+
runtime.loadTools(ctx, deps.getResolutionContext?.());
|
|
267
|
+
ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
notifyFailure(ctx);
|
|
271
|
+
reportCallbackError(error);
|
|
272
|
+
}
|
|
273
|
+
}, deps.reloadDelayMs ?? 150);
|
|
254
274
|
reloadTimeout.unref?.();
|
|
255
275
|
};
|
|
256
276
|
const watchParent = (ctx, recipeRoot) => {
|
package/lib/extension-runtime.ts
CHANGED
|
@@ -35,6 +35,8 @@ export function createActorExtensionRuntime(
|
|
|
35
35
|
pi: Pi.ExtensionAPI,
|
|
36
36
|
): ActorExtensionRuntime {
|
|
37
37
|
let activeRunContext: Pi.ExtensionContext | undefined;
|
|
38
|
+
let activeRunOwnerId: string | undefined;
|
|
39
|
+
const runOwnerIdsByContext = new WeakMap<Pi.ExtensionContext, string>();
|
|
38
40
|
const recipeResolutionContextsBySession = new Map<
|
|
39
41
|
string,
|
|
40
42
|
RecipeResolution.RecipeResolutionContext
|
|
@@ -55,9 +57,20 @@ export function createActorExtensionRuntime(
|
|
|
55
57
|
getRunOwnerId,
|
|
56
58
|
getThinkingLevel: () => pi.getThinkingLevel(),
|
|
57
59
|
});
|
|
58
|
-
|
|
60
|
+
let recipeReload: Runtime.RecipeToolReloadWatcher | undefined;
|
|
61
|
+
let runUiRuntime: RunUiRuntime.RunUiRuntime;
|
|
62
|
+
const closeActiveSessionRuntimes = (): void => {
|
|
63
|
+
const ownerId = activeRunOwnerId;
|
|
64
|
+
activeRunContext = undefined;
|
|
65
|
+
activeRunOwnerId = undefined;
|
|
66
|
+
runUiRuntime?.close();
|
|
67
|
+
automaticReview.close();
|
|
68
|
+
recipeReload?.close();
|
|
69
|
+
if (ownerId) recipeResolutionContextsBySession.delete(ownerId);
|
|
70
|
+
};
|
|
71
|
+
runUiRuntime = RunUiRuntime.createRunUiRuntime({
|
|
59
72
|
getActiveContext: () => activeRunContext,
|
|
60
|
-
|
|
73
|
+
onCallbackError: closeActiveSessionRuntimes,
|
|
61
74
|
onRunEvent: automaticReview.schedule,
|
|
62
75
|
pi,
|
|
63
76
|
});
|
|
@@ -102,11 +115,12 @@ export function createActorExtensionRuntime(
|
|
|
102
115
|
reservedToolNames: Tools.RESERVED_TOOL_NAMES,
|
|
103
116
|
setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
|
|
104
117
|
});
|
|
105
|
-
|
|
118
|
+
recipeReload = Runtime.createRecipeToolReloadWatcher(runtime, {
|
|
106
119
|
getResolutionContext: () =>
|
|
107
120
|
activeRunContext
|
|
108
121
|
? getRecipeResolutionContext(activeRunContext)
|
|
109
122
|
: undefined,
|
|
123
|
+
onCallbackError: closeActiveSessionRuntimes,
|
|
110
124
|
});
|
|
111
125
|
return {
|
|
112
126
|
beforeAgentStart(systemPrompt, skills, ctx) {
|
|
@@ -131,11 +145,11 @@ export function createActorExtensionRuntime(
|
|
|
131
145
|
if (activeRunContext === ctx) automaticReview.schedule();
|
|
132
146
|
},
|
|
133
147
|
onSessionShutdown(reason, ctx) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
runUiRuntime.shutdown(reason, ctx);
|
|
148
|
+
const ownerId = runOwnerIdsByContext.get(ctx);
|
|
149
|
+
runOwnerIdsByContext.delete(ctx);
|
|
150
|
+
if (activeRunContext === ctx) closeActiveSessionRuntimes();
|
|
151
|
+
if (ownerId) recipeResolutionContextsBySession.delete(ownerId);
|
|
152
|
+
runUiRuntime.shutdown(reason, ownerId, ctx);
|
|
139
153
|
},
|
|
140
154
|
async onSessionStart(ctx) {
|
|
141
155
|
const sessionId = getRunOwnerId(ctx);
|
|
@@ -145,13 +159,15 @@ export function createActorExtensionRuntime(
|
|
|
145
159
|
);
|
|
146
160
|
ctx.ui.setWidget("zz-pi-actors-comms", undefined);
|
|
147
161
|
activeRunContext = ctx;
|
|
162
|
+
activeRunOwnerId = sessionId;
|
|
163
|
+
runOwnerIdsByContext.set(ctx, sessionId);
|
|
148
164
|
runUiRuntime.close();
|
|
149
165
|
automaticReview.close();
|
|
150
166
|
recipeReload.close();
|
|
151
167
|
await Temp.prepareExtensionTempDir(Paths.EXTENSION_RUNTIME_PATHS.tempDir);
|
|
152
|
-
if (activeRunContext !== ctx) return;
|
|
168
|
+
if (activeRunContext !== ctx || activeRunOwnerId !== sessionId) return;
|
|
153
169
|
automaticReview.start(ctx);
|
|
154
|
-
runUiRuntime.start(ctx);
|
|
170
|
+
runUiRuntime.start(ctx, sessionId);
|
|
155
171
|
recipeReload.watch(ctx);
|
|
156
172
|
},
|
|
157
173
|
registerCoreTools() {
|
package/lib/observability.ts
CHANGED
|
@@ -418,7 +418,11 @@ export function createRunTerminalReconciliationLoop(input: {
|
|
|
418
418
|
input.refreshWatcher();
|
|
419
419
|
input.reconcile();
|
|
420
420
|
} catch (error) {
|
|
421
|
-
|
|
421
|
+
try {
|
|
422
|
+
input.onError?.(error);
|
|
423
|
+
} catch {
|
|
424
|
+
/* reconciliation callbacks must never escape into the host event loop */
|
|
425
|
+
}
|
|
422
426
|
}
|
|
423
427
|
};
|
|
424
428
|
const close = (): void => {
|
package/lib/run-ui-runtime.ts
CHANGED
|
@@ -11,25 +11,89 @@ import * as Pi from "./pi.ts";
|
|
|
11
11
|
|
|
12
12
|
export interface RunUiRuntime {
|
|
13
13
|
close(): void;
|
|
14
|
-
shutdown(
|
|
15
|
-
|
|
14
|
+
shutdown(
|
|
15
|
+
eventReason: string,
|
|
16
|
+
ownerId: string | undefined,
|
|
17
|
+
ctx?: Pi.ExtensionContext,
|
|
18
|
+
): void;
|
|
19
|
+
start(ctx: Pi.ExtensionContext, ownerId: string): void;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export interface RunUiRuntimeDeps {
|
|
23
|
+
animationIntervalMs?: number;
|
|
24
|
+
createRunStateWatcher?: typeof Observability.createRunStateWatcher;
|
|
25
|
+
createRunTerminalReconciliationLoop?:
|
|
26
|
+
typeof Observability.createRunTerminalReconciliationLoop;
|
|
19
27
|
getActiveContext(): Pi.ExtensionContext | undefined;
|
|
20
|
-
|
|
28
|
+
notificationDelayMs?: number;
|
|
29
|
+
onCallbackError?: (error: unknown) => void;
|
|
21
30
|
onRunEvent(): void;
|
|
22
31
|
pi: Pi.ExtensionAPI;
|
|
32
|
+
teardownRunsOwnedByParent?: typeof AsyncRuns.teardownRunsOwnedByParent;
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
36
|
+
let activeContext: Pi.ExtensionContext | undefined;
|
|
37
|
+
let activeOwnerId: string | undefined;
|
|
26
38
|
let animationInterval: NodeJS.Timeout | undefined;
|
|
27
39
|
let notifyTimeout: NodeJS.Timeout | undefined;
|
|
40
|
+
let running = false;
|
|
28
41
|
let lastWatcherDiagnosticId = 0;
|
|
29
42
|
const observation = Observability.createRunUiObservationState();
|
|
30
43
|
const retirementAttempts = new Set<string>();
|
|
31
44
|
const terminalNotificationsInFlight = new Set<string>();
|
|
32
45
|
|
|
46
|
+
const close = (): void => {
|
|
47
|
+
running = false;
|
|
48
|
+
activeContext = undefined;
|
|
49
|
+
activeOwnerId = undefined;
|
|
50
|
+
try {
|
|
51
|
+
watcher.close();
|
|
52
|
+
} catch {
|
|
53
|
+
/* cleanup must not escape a host callback */
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
reconciliation.close();
|
|
57
|
+
} catch {
|
|
58
|
+
/* cleanup must not escape a host callback */
|
|
59
|
+
}
|
|
60
|
+
if (notifyTimeout) clearTimeout(notifyTimeout);
|
|
61
|
+
notifyTimeout = undefined;
|
|
62
|
+
if (animationInterval) clearInterval(animationInterval);
|
|
63
|
+
animationInterval = undefined;
|
|
64
|
+
};
|
|
65
|
+
const stopAfterCallbackFailure = (
|
|
66
|
+
label: string,
|
|
67
|
+
error: unknown,
|
|
68
|
+
expectedContext: Pi.ExtensionContext,
|
|
69
|
+
): void => {
|
|
70
|
+
if (activeContext !== expectedContext) return;
|
|
71
|
+
close();
|
|
72
|
+
try {
|
|
73
|
+
deps.onCallbackError?.(error);
|
|
74
|
+
} catch {
|
|
75
|
+
/* host callback containment must remain no-throw */
|
|
76
|
+
}
|
|
77
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
78
|
+
try {
|
|
79
|
+
expectedContext.ui.notify(`Actor ${label} failed: ${message}`, "error");
|
|
80
|
+
} catch {
|
|
81
|
+
/* stale context or unavailable UI */
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const runActiveCallback = (
|
|
85
|
+
label: string,
|
|
86
|
+
callback: (ctx: Pi.ExtensionContext, ownerId: string) => void,
|
|
87
|
+
): void => {
|
|
88
|
+
if (!running || !activeContext || !activeOwnerId) return;
|
|
89
|
+
const ctx = activeContext;
|
|
90
|
+
try {
|
|
91
|
+
if (deps.getActiveContext() !== ctx) return;
|
|
92
|
+
callback(ctx, activeOwnerId);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
stopAfterCallbackFailure(label, error, ctx);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
33
97
|
const retireCandidateRuns = (
|
|
34
98
|
ctx: Pi.ExtensionContext,
|
|
35
99
|
summary: Observability.RunSummary,
|
|
@@ -39,14 +103,16 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
39
103
|
cancelRun: (candidate) => AsyncRuns.cancelRun(candidate.stateDir),
|
|
40
104
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
41
105
|
sendStop: async (candidate) => AsyncRuns.cancelRun(candidate.stateDir),
|
|
42
|
-
})
|
|
106
|
+
}).catch((error) =>
|
|
107
|
+
stopAfterCallbackFailure("Run retirement callback", error, ctx),
|
|
108
|
+
);
|
|
43
109
|
};
|
|
44
110
|
const update = (
|
|
45
111
|
ctx: Pi.ExtensionContext,
|
|
112
|
+
ownerId: string,
|
|
46
113
|
notify = false,
|
|
47
114
|
terminalOnly = false,
|
|
48
115
|
): void => {
|
|
49
|
-
const ownerId = deps.getRunOwnerId(ctx);
|
|
50
116
|
const snapshot = Observability.readRunUiSnapshot(observation, ownerId);
|
|
51
117
|
ctx.ui.setStatus(
|
|
52
118
|
"zz-pi-actors-runs",
|
|
@@ -76,61 +142,63 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
76
142
|
}
|
|
77
143
|
};
|
|
78
144
|
const scheduleUpdate = (): void => {
|
|
145
|
+
if (!running) return;
|
|
79
146
|
if (notifyTimeout) clearTimeout(notifyTimeout);
|
|
80
147
|
notifyTimeout = setTimeout(() => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}, 50);
|
|
148
|
+
runActiveCallback("Run watcher callback", (ctx, ownerId) => {
|
|
149
|
+
watcher.refresh();
|
|
150
|
+
update(ctx, ownerId, true);
|
|
151
|
+
deps.onRunEvent();
|
|
152
|
+
reportDiagnostics(ctx);
|
|
153
|
+
});
|
|
154
|
+
}, deps.notificationDelayMs ?? 50);
|
|
88
155
|
notifyTimeout.unref?.();
|
|
89
156
|
};
|
|
90
|
-
const watcher = Observability.createRunStateWatcher({
|
|
157
|
+
const watcher = (deps.createRunStateWatcher ?? Observability.createRunStateWatcher)({
|
|
91
158
|
stateRoot: Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
|
|
92
159
|
onChange: scheduleUpdate,
|
|
93
160
|
});
|
|
94
|
-
const reconciliation =
|
|
161
|
+
const reconciliation = (
|
|
162
|
+
deps.createRunTerminalReconciliationLoop ??
|
|
163
|
+
Observability.createRunTerminalReconciliationLoop
|
|
164
|
+
)({
|
|
95
165
|
onError: (error) => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
166
|
+
if (!running || !activeContext) return;
|
|
167
|
+
stopAfterCallbackFailure(
|
|
168
|
+
"terminal reconciliation callback",
|
|
169
|
+
error,
|
|
170
|
+
activeContext,
|
|
171
|
+
);
|
|
100
172
|
},
|
|
101
173
|
reconcile: () => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
174
|
+
runActiveCallback("terminal reconciliation callback", (ctx, ownerId) => {
|
|
175
|
+
Observability.reconcileRunTerminalNotifications({
|
|
176
|
+
inFlight: terminalNotificationsInFlight,
|
|
177
|
+
ownerId,
|
|
178
|
+
sink: Pi.createNotificationSink(deps.pi, ctx),
|
|
179
|
+
state: observation,
|
|
180
|
+
includeAttention: true,
|
|
181
|
+
});
|
|
182
|
+
reportDiagnostics(ctx);
|
|
110
183
|
});
|
|
111
|
-
reportDiagnostics(ctx);
|
|
112
184
|
},
|
|
113
|
-
refreshWatcher: () =>
|
|
185
|
+
refreshWatcher: () => {
|
|
186
|
+
if (running) watcher.refresh();
|
|
187
|
+
},
|
|
114
188
|
});
|
|
115
|
-
const close = (): void => {
|
|
116
|
-
watcher.close();
|
|
117
|
-
reconciliation.close();
|
|
118
|
-
if (notifyTimeout) clearTimeout(notifyTimeout);
|
|
119
|
-
notifyTimeout = undefined;
|
|
120
|
-
if (animationInterval) clearInterval(animationInterval);
|
|
121
|
-
animationInterval = undefined;
|
|
122
|
-
};
|
|
123
189
|
|
|
124
190
|
return {
|
|
125
191
|
close,
|
|
126
|
-
shutdown(eventReason, ctx) {
|
|
127
|
-
|
|
128
|
-
const teardown =
|
|
129
|
-
deps.
|
|
192
|
+
shutdown(eventReason, ownerId, ctx) {
|
|
193
|
+
if (!ownerId) return;
|
|
194
|
+
const teardown = (
|
|
195
|
+
deps.teardownRunsOwnedByParent ?? AsyncRuns.teardownRunsOwnedByParent
|
|
196
|
+
)(
|
|
197
|
+
ownerId,
|
|
130
198
|
Paths.EXTENSION_RUNTIME_PATHS.runStateRoot,
|
|
131
199
|
{ trigger: `session_shutdown:${eventReason}` },
|
|
132
200
|
);
|
|
133
|
-
if (teardown.failed === 0) return;
|
|
201
|
+
if (teardown.failed === 0 || !ctx) return;
|
|
134
202
|
try {
|
|
135
203
|
ctx.ui.notify(
|
|
136
204
|
`Actor shutdown teardown: killed=${teardown.killed} failed=${teardown.failed} skipped=${teardown.skipped} discovery_failed=${teardown.discoveryFailed}. Summary: ${teardown.summaryPath ?? "unavailable"}.`,
|
|
@@ -140,16 +208,26 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
140
208
|
/* stale shutdown context */
|
|
141
209
|
}
|
|
142
210
|
},
|
|
143
|
-
start(ctx) {
|
|
211
|
+
start(ctx, ownerId) {
|
|
144
212
|
close();
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
213
|
+
activeContext = ctx;
|
|
214
|
+
activeOwnerId = ownerId;
|
|
215
|
+
running = true;
|
|
216
|
+
try {
|
|
217
|
+
Observability.primeRunAttentionState(observation, ownerId);
|
|
218
|
+
update(ctx, ownerId, true, true);
|
|
219
|
+
watcher.refresh();
|
|
220
|
+
reconciliation.start();
|
|
221
|
+
animationInterval = setInterval(() => {
|
|
222
|
+
runActiveCallback("status animation callback", (current, currentOwnerId) =>
|
|
223
|
+
update(current, currentOwnerId),
|
|
224
|
+
);
|
|
225
|
+
}, deps.animationIntervalMs ?? 1000);
|
|
226
|
+
animationInterval.unref?.();
|
|
227
|
+
} catch (error) {
|
|
228
|
+
close();
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
153
231
|
},
|
|
154
232
|
};
|
|
155
233
|
}
|
package/lib/runtime.ts
CHANGED
|
@@ -308,7 +308,9 @@ export function createAutoToolsRuntime(
|
|
|
308
308
|
export interface RecipeToolReloadWatcherDeps {
|
|
309
309
|
exists?: (path: string) => boolean;
|
|
310
310
|
getResolutionContext?: () => RecipeResolutionContext | undefined;
|
|
311
|
+
onCallbackError?: (error: unknown) => void;
|
|
311
312
|
recipeRoot?: string;
|
|
313
|
+
reloadDelayMs?: number;
|
|
312
314
|
watchPath?: typeof watch;
|
|
313
315
|
}
|
|
314
316
|
|
|
@@ -336,22 +338,39 @@ export function createRecipeToolReloadWatcher(
|
|
|
336
338
|
reloadTimeout = undefined;
|
|
337
339
|
setWatchStatus("closed");
|
|
338
340
|
};
|
|
341
|
+
const reportCallbackError = (error: unknown): void => {
|
|
342
|
+
try {
|
|
343
|
+
deps.onCallbackError?.(error);
|
|
344
|
+
} catch {
|
|
345
|
+
/* host callback containment must remain no-throw */
|
|
346
|
+
}
|
|
347
|
+
};
|
|
339
348
|
const notifyFailure = (ctx: RuntimeContext): void => {
|
|
340
349
|
if (failureNotified) return;
|
|
341
350
|
failureNotified = true;
|
|
342
351
|
setWatchStatus("failed");
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
352
|
+
try {
|
|
353
|
+
ctx.ui.notify(
|
|
354
|
+
"Recipe live reload watcher failed; restart the session or use register_tool again to refresh recipe tools.",
|
|
355
|
+
"warning",
|
|
356
|
+
);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
reportCallbackError(error);
|
|
359
|
+
}
|
|
347
360
|
};
|
|
348
361
|
const scheduleReload = (ctx: RuntimeContext): void => {
|
|
349
362
|
failureNotified = false;
|
|
350
363
|
if (reloadTimeout) clearTimeout(reloadTimeout);
|
|
351
364
|
reloadTimeout = setTimeout(() => {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
365
|
+
reloadTimeout = undefined;
|
|
366
|
+
try {
|
|
367
|
+
runtime.loadTools(ctx, deps.getResolutionContext?.());
|
|
368
|
+
ctx.ui.notify("Recipe tools refreshed from ~/.pi/agent/recipes", "info");
|
|
369
|
+
} catch (error) {
|
|
370
|
+
notifyFailure(ctx);
|
|
371
|
+
reportCallbackError(error);
|
|
372
|
+
}
|
|
373
|
+
}, deps.reloadDelayMs ?? 150);
|
|
355
374
|
reloadTimeout.unref?.();
|
|
356
375
|
};
|
|
357
376
|
const watchParent = (ctx: RuntimeContext, recipeRoot: string): void => {
|