@llblab/pi-actors 0.49.0 → 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 +8 -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/dist/skills/music-player/SKILL.md +6 -2
- 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/skills/music-player/SKILL.md +6 -2
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,14 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.49.1: Maintained Telegram View Routing
|
|
12
|
+
|
|
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.
|
|
14
|
+
|
|
7
15
|
## 0.49.0: Skill-Scoped Music Player
|
|
8
16
|
|
|
9
17
|
- `Skill-Scoped Singletons`: Added one optional async singleton Recipe per active Skill with canonical `run:<skill>` and `<skill>/<recipe>` identities, idempotent compatible reuse, lifecycle/process fencing, terminal-generation replacement, delegation-safe identity inheritance, persistent actor-owned state directories, focused inspection, and fail-closed conflicts across Recipe, owner, startup values, and Control.
|
|
@@ -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) => {
|
|
@@ -7,6 +7,10 @@ description: Use for starting, resuming, inspecting, and controlling one persist
|
|
|
7
7
|
|
|
8
8
|
Use this Skill for one local music playback service. For generic Recipe execution, singleton Run lifecycle, persistent-tool setup, or diagnosis, follow `actors`; this Skill owns only playback-specific selection and controls.
|
|
9
9
|
|
|
10
|
+
## Interaction Routing
|
|
11
|
+
|
|
12
|
+
When a Telegram-originated turn or explicit Telegram-control question makes repeated player controls relevant, prefer the maintained Telegram view described below over synthesizing one-shot prompt buttons. If `telegram_bind` is available, load and follow the active operating guidance that owns Generative Apps, then bind or invoke the capability-owned adapter; do not re-author it or move playback authority into the view. If the runtime is unavailable or binding fails, report that boundary and fall back to ordinary model-mediated controls.
|
|
13
|
+
|
|
10
14
|
## Playback
|
|
11
15
|
|
|
12
16
|
`music-player/playback` is a singleton async controlled service with the canonical address `run:music-player`. The Recipe is the sole lifecycle owner: it starts the service, supervises it, and stops playback when the Run closes. The service owns queue, backend, checkpoint, and playback state. `playback-client.mjs` is a pure actor-neutral RPC client; it never starts, adopts, supervises, or signals a service. The executable also supports explicit foreground `serve` for development or a caller-owned standalone host, but Actor and standalone ownership of one state directory are mutually exclusive.
|
|
@@ -33,10 +37,10 @@ Use only declared actions: `play`, `pause`, `resume`, `toggle`, `next`, `previou
|
|
|
33
37
|
|
|
34
38
|
External local views use the actor-neutral playback client against the canonical service state directory. They must not read or edit Run files, signal playback processes, construct Actor Control records, or import pi-actors internals. The client validates bounded commands, exact service generation, structured responses, and active endpoint ownership before returning success.
|
|
35
39
|
|
|
36
|
-
##
|
|
40
|
+
## Maintained Telegram View
|
|
37
41
|
|
|
38
42
|
> [!NOTE]
|
|
39
|
-
>
|
|
43
|
+
> This Skill includes a ready Music Player Generative App at `genapps/music-player.mjs`. When `telegram_bind` is available and Telegram interaction is relevant, use it to copy and install the app as `music-player`; hot replacement keeps the same app name with `replace: true`.
|
|
40
44
|
|
|
41
45
|
Bind with absolute `control`, `stateDir`, and `node` arguments. The adapter reports whether the Actor control surface is actually available. Every mutating button queues a canonical Actor Control through `playback.mjs` and waits for that exact record to become handled or failed, so terminal evidence remains visible in the Run inspector and failures reach Telegram; bounded status projection is read-only. The adapter neither imports extension internals nor starts playback. Its stopped-state Start button returns to Pi so the composition root can spawn `music-player/playback`, while active controls remain deterministic Generative App actions that bypass the model. `pi-telegram` owns only the generic Generative App runtime.
|
|
42
46
|
|
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 => {
|
package/package.json
CHANGED
|
@@ -7,6 +7,10 @@ description: Use for starting, resuming, inspecting, and controlling one persist
|
|
|
7
7
|
|
|
8
8
|
Use this Skill for one local music playback service. For generic Recipe execution, singleton Run lifecycle, persistent-tool setup, or diagnosis, follow `actors`; this Skill owns only playback-specific selection and controls.
|
|
9
9
|
|
|
10
|
+
## Interaction Routing
|
|
11
|
+
|
|
12
|
+
When a Telegram-originated turn or explicit Telegram-control question makes repeated player controls relevant, prefer the maintained Telegram view described below over synthesizing one-shot prompt buttons. If `telegram_bind` is available, load and follow the active operating guidance that owns Generative Apps, then bind or invoke the capability-owned adapter; do not re-author it or move playback authority into the view. If the runtime is unavailable or binding fails, report that boundary and fall back to ordinary model-mediated controls.
|
|
13
|
+
|
|
10
14
|
## Playback
|
|
11
15
|
|
|
12
16
|
`music-player/playback` is a singleton async controlled service with the canonical address `run:music-player`. The Recipe is the sole lifecycle owner: it starts the service, supervises it, and stops playback when the Run closes. The service owns queue, backend, checkpoint, and playback state. `playback-client.mjs` is a pure actor-neutral RPC client; it never starts, adopts, supervises, or signals a service. The executable also supports explicit foreground `serve` for development or a caller-owned standalone host, but Actor and standalone ownership of one state directory are mutually exclusive.
|
|
@@ -33,10 +37,10 @@ Use only declared actions: `play`, `pause`, `resume`, `toggle`, `next`, `previou
|
|
|
33
37
|
|
|
34
38
|
External local views use the actor-neutral playback client against the canonical service state directory. They must not read or edit Run files, signal playback processes, construct Actor Control records, or import pi-actors internals. The client validates bounded commands, exact service generation, structured responses, and active endpoint ownership before returning success.
|
|
35
39
|
|
|
36
|
-
##
|
|
40
|
+
## Maintained Telegram View
|
|
37
41
|
|
|
38
42
|
> [!NOTE]
|
|
39
|
-
>
|
|
43
|
+
> This Skill includes a ready Music Player Generative App at `genapps/music-player.mjs`. When `telegram_bind` is available and Telegram interaction is relevant, use it to copy and install the app as `music-player`; hot replacement keeps the same app name with `replace: true`.
|
|
40
44
|
|
|
41
45
|
Bind with absolute `control`, `stateDir`, and `node` arguments. The adapter reports whether the Actor control surface is actually available. Every mutating button queues a canonical Actor Control through `playback.mjs` and waits for that exact record to become handled or failed, so terminal evidence remains visible in the Run inspector and failures reach Telegram; bounded status projection is read-only. The adapter neither imports extension internals nor starts playback. Its stopped-state Start button returns to Pi so the composition root can spawn `music-player/playback`, while active controls remain deterministic Generative App actions that bypass the model. `pi-telegram` owns only the generic Generative App runtime.
|
|
42
46
|
|