@llblab/pi-kit 0.1.4 → 0.1.6
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/CHANGELOG.md +9 -0
- package/README.md +5 -5
- package/node_modules/@llblab/pi-actors/BACKLOG.md +1 -1
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +14 -1
- package/node_modules/@llblab/pi-actors/README.md +19 -3
- package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +29 -10
- package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.d.ts +3 -0
- package/node_modules/@llblab/pi-actors/dist/lib/inspector-overlay.js +120 -38
- package/node_modules/@llblab/pi-actors/dist/lib/limits.d.ts +1 -0
- package/node_modules/@llblab/pi-actors/dist/lib/limits.js +1 -0
- package/node_modules/@llblab/pi-actors/dist/lib/observability.js +6 -1
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.d.ts +10 -3
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +109 -52
- package/node_modules/@llblab/pi-actors/dist/lib/runs-artifacts.js +24 -4
- package/node_modules/@llblab/pi-actors/dist/lib/runtime.d.ts +2 -0
- package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +24 -4
- package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.d.ts +1 -0
- package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.js +3 -2
- package/node_modules/@llblab/pi-actors/dist/lib/state-readers.d.ts +5 -1
- package/node_modules/@llblab/pi-actors/dist/lib/state-readers.js +30 -3
- package/node_modules/@llblab/pi-actors/dist/lib/tools-access.d.ts +1 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-access.js +13 -11
- package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.js +5 -5
- package/node_modules/@llblab/pi-actors/dist/lib/tools-message.d.ts +1 -0
- package/node_modules/@llblab/pi-actors/dist/lib/tools-message.js +3 -1
- package/node_modules/@llblab/pi-actors/docs/README.md +1 -0
- package/node_modules/@llblab/pi-actors/docs/actor-inspector.md +21 -5
- package/node_modules/@llblab/pi-actors/docs/async-runs.md +16 -4
- package/node_modules/@llblab/pi-actors/docs/command-templates.md +5 -4
- package/node_modules/@llblab/pi-actors/docs/inspection.md +83 -0
- package/node_modules/@llblab/pi-actors/docs/recipe-library.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/template-recipes.md +225 -66
- package/node_modules/@llblab/pi-actors/docs/tool-registry.md +24 -3
- package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +26 -10
- package/node_modules/@llblab/pi-actors/lib/inspector-overlay.ts +161 -33
- package/node_modules/@llblab/pi-actors/lib/limits.ts +1 -0
- package/node_modules/@llblab/pi-actors/lib/observability.ts +5 -1
- package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +128 -50
- package/node_modules/@llblab/pi-actors/lib/runs-artifacts.ts +34 -4
- package/node_modules/@llblab/pi-actors/lib/runtime.ts +26 -7
- package/node_modules/@llblab/pi-actors/lib/session-evidence.ts +7 -2
- package/node_modules/@llblab/pi-actors/lib/state-readers.ts +45 -3
- package/node_modules/@llblab/pi-actors/lib/tools-access.ts +26 -12
- package/node_modules/@llblab/pi-actors/lib/tools-inspect.ts +9 -5
- package/node_modules/@llblab/pi-actors/lib/tools-message.ts +8 -1
- package/node_modules/@llblab/pi-actors/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +3 -3
- package/node_modules/@llblab/pi-telegram/index.ts +25 -0
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +192 -9
- package/node_modules/@llblab/pi-telegram/lib/runtime.ts +52 -2
- package/node_modules/@llblab/pi-telegram/lib/status.ts +3 -3
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +5 -1
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +14 -6
- package/package.json +4 -4
|
@@ -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
|
}
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
closeSync,
|
|
9
|
+
fstatSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readSync,
|
|
12
|
+
} from "node:fs";
|
|
8
13
|
|
|
9
14
|
import { substituteCommandTemplateToken } from "./command-templates.ts";
|
|
10
15
|
|
|
@@ -22,6 +27,31 @@ export interface RunArtifactManifestEntry {
|
|
|
22
27
|
size?: number;
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
function hashArtifactFile(path: string): { sha256: string; size: number } {
|
|
31
|
+
const fd = openSync(path, "r");
|
|
32
|
+
try {
|
|
33
|
+
const size = fstatSync(fd).size;
|
|
34
|
+
const hash = createHash("sha256");
|
|
35
|
+
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
36
|
+
let position = 0;
|
|
37
|
+
while (position < size) {
|
|
38
|
+
const bytesRead = readSync(
|
|
39
|
+
fd,
|
|
40
|
+
chunk,
|
|
41
|
+
0,
|
|
42
|
+
Math.min(chunk.byteLength, size - position),
|
|
43
|
+
position,
|
|
44
|
+
);
|
|
45
|
+
if (bytesRead === 0) break;
|
|
46
|
+
hash.update(chunk.subarray(0, bytesRead));
|
|
47
|
+
position += bytesRead;
|
|
48
|
+
}
|
|
49
|
+
return { sha256: hash.digest("hex"), size: position };
|
|
50
|
+
} finally {
|
|
51
|
+
closeSync(fd);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
25
55
|
export function resolveArtifactPaths(
|
|
26
56
|
artifacts: Record<string, RunArtifactDeclaration> | undefined,
|
|
27
57
|
values: Record<string, unknown>,
|
|
@@ -64,7 +94,7 @@ export function resolveArtifactManifest(
|
|
|
64
94
|
typeof artifact === "string" ? { path: artifact } : artifact;
|
|
65
95
|
if (!declaration?.path) continue;
|
|
66
96
|
try {
|
|
67
|
-
const
|
|
97
|
+
const hashed = hashArtifactFile(declaration.path);
|
|
68
98
|
manifest[name] = {
|
|
69
99
|
exists: true,
|
|
70
100
|
...(declaration.kind ? { kind: declaration.kind } : {}),
|
|
@@ -75,8 +105,8 @@ export function resolveArtifactManifest(
|
|
|
75
105
|
...(declaration.required !== undefined
|
|
76
106
|
? { required: declaration.required }
|
|
77
107
|
: {}),
|
|
78
|
-
sha256:
|
|
79
|
-
size:
|
|
108
|
+
sha256: hashed.sha256,
|
|
109
|
+
size: hashed.size,
|
|
80
110
|
};
|
|
81
111
|
} catch {
|
|
82
112
|
manifest[name] = {
|
|
@@ -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 => {
|
|
@@ -55,6 +55,7 @@ export interface SessionEvidence {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export interface SessionEvidenceReadOptions {
|
|
58
|
+
maxBytes?: number;
|
|
58
59
|
maxTextChars?: number;
|
|
59
60
|
maxToolCalls?: number;
|
|
60
61
|
maxTurns?: number;
|
|
@@ -195,6 +196,10 @@ export function readSessionEvidence(
|
|
|
195
196
|
path: string,
|
|
196
197
|
options: SessionEvidenceReadOptions = {},
|
|
197
198
|
): SessionEvidence {
|
|
199
|
+
const maxBytes = Math.max(
|
|
200
|
+
1,
|
|
201
|
+
options.maxBytes ?? Limits.SESSION_EVIDENCE_MAX_BYTES,
|
|
202
|
+
);
|
|
198
203
|
const maxTextChars = Math.max(
|
|
199
204
|
1,
|
|
200
205
|
options.maxTextChars ?? Limits.SESSION_EVIDENCE_TEXT_CHARS,
|
|
@@ -207,7 +212,7 @@ export function readSessionEvidence(
|
|
|
207
212
|
1,
|
|
208
213
|
options.maxTurns ?? Limits.SESSION_EVIDENCE_MAX_TURNS,
|
|
209
214
|
);
|
|
210
|
-
const read = readJsonlFileResilient<SessionEntry>(path);
|
|
215
|
+
const read = readJsonlFileResilient<SessionEntry>(path, { maxBytes });
|
|
211
216
|
const diagnostics = [...read.diagnostics];
|
|
212
217
|
const header = read.records.find((entry) => entry.type === "session");
|
|
213
218
|
const branch = activeBranch(read.records, path, diagnostics);
|
|
@@ -296,7 +301,7 @@ export function readSessionEvidence(
|
|
|
296
301
|
path,
|
|
297
302
|
...(header ? { session: asRecord(header) } : {}),
|
|
298
303
|
totalTurns: turns.length,
|
|
299
|
-
truncated: turns.length > visibleTurns.length,
|
|
304
|
+
truncated: read.truncated === true || turns.length > visibleTurns.length,
|
|
300
305
|
turns: visibleTurns,
|
|
301
306
|
};
|
|
302
307
|
}
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
* Owns best-effort JSON and JSONL parsing helpers for operator-facing state reads.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
closeSync,
|
|
9
|
+
fstatSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
readSync,
|
|
13
|
+
} from "node:fs";
|
|
8
14
|
|
|
9
15
|
export interface StateReadDiagnostic {
|
|
10
16
|
line?: number;
|
|
@@ -20,6 +26,11 @@ export interface JsonReadResult<T> {
|
|
|
20
26
|
export interface JsonlReadResult<T> {
|
|
21
27
|
diagnostics: StateReadDiagnostic[];
|
|
22
28
|
records: T[];
|
|
29
|
+
truncated?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface JsonlReadOptions {
|
|
33
|
+
maxBytes?: number;
|
|
23
34
|
}
|
|
24
35
|
|
|
25
36
|
function isEnoent(error: unknown): boolean {
|
|
@@ -30,6 +41,31 @@ function diagnosticMessage(error: unknown): string {
|
|
|
30
41
|
return error instanceof Error ? error.message : String(error);
|
|
31
42
|
}
|
|
32
43
|
|
|
44
|
+
function readTextFileCapped(path: string, maxBytes: number | undefined): string {
|
|
45
|
+
if (maxBytes === undefined) return readFileSync(path, "utf8");
|
|
46
|
+
const limit = Math.max(0, Math.floor(maxBytes));
|
|
47
|
+
const fd = openSync(path, "r");
|
|
48
|
+
try {
|
|
49
|
+
const size = fstatSync(fd).size;
|
|
50
|
+
if (size > limit) {
|
|
51
|
+
throw Object.assign(
|
|
52
|
+
new Error(`file exceeds bounded read limit (${size} > ${limit} bytes)`),
|
|
53
|
+
{ code: "EFBIG" },
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const content = Buffer.allocUnsafe(size);
|
|
57
|
+
let offset = 0;
|
|
58
|
+
while (offset < size) {
|
|
59
|
+
const bytesRead = readSync(fd, content, offset, size - offset, offset);
|
|
60
|
+
if (bytesRead === 0) break;
|
|
61
|
+
offset += bytesRead;
|
|
62
|
+
}
|
|
63
|
+
return content.subarray(0, offset).toString("utf8");
|
|
64
|
+
} finally {
|
|
65
|
+
closeSync(fd);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
33
69
|
export function readJsonFileResilient<T>(path: string, fallback: T): JsonReadResult<T> {
|
|
34
70
|
try {
|
|
35
71
|
return {
|
|
@@ -50,11 +86,14 @@ export function readJsonFileResilient<T>(path: string, fallback: T): JsonReadRes
|
|
|
50
86
|
}
|
|
51
87
|
}
|
|
52
88
|
|
|
53
|
-
export function readJsonlFileResilient<T>(
|
|
89
|
+
export function readJsonlFileResilient<T>(
|
|
90
|
+
path: string,
|
|
91
|
+
options: JsonlReadOptions = {},
|
|
92
|
+
): JsonlReadResult<T> {
|
|
54
93
|
try {
|
|
55
94
|
const records: T[] = [];
|
|
56
95
|
const diagnostics: StateReadDiagnostic[] = [];
|
|
57
|
-
const lines =
|
|
96
|
+
const lines = readTextFileCapped(path, options.maxBytes).split("\n");
|
|
58
97
|
for (const [index, line] of lines.entries()) {
|
|
59
98
|
if (!line.trim()) continue;
|
|
60
99
|
try {
|
|
@@ -78,6 +117,9 @@ export function readJsonlFileResilient<T>(path: string): JsonlReadResult<T> {
|
|
|
78
117
|
},
|
|
79
118
|
],
|
|
80
119
|
records: [],
|
|
120
|
+
...((error as NodeJS.ErrnoException).code === "EFBIG"
|
|
121
|
+
? { truncated: true }
|
|
122
|
+
: {}),
|
|
81
123
|
};
|
|
82
124
|
}
|
|
83
125
|
}
|
|
@@ -17,8 +17,11 @@ export function getContextSessionId(ctx: unknown): string | undefined {
|
|
|
17
17
|
export function requireContextSessionId(ctx: unknown, actor: string): string {
|
|
18
18
|
const sessionId = getContextSessionId(ctx);
|
|
19
19
|
if (!sessionId) {
|
|
20
|
-
throw
|
|
21
|
-
|
|
20
|
+
throw Object.assign(
|
|
21
|
+
new Error(
|
|
22
|
+
`${actor} reason=session_unavailable requires a current coordinator session; retry from an active coordinator session.`,
|
|
23
|
+
),
|
|
24
|
+
{ reason: "session_unavailable" },
|
|
22
25
|
);
|
|
23
26
|
}
|
|
24
27
|
return sessionId;
|
|
@@ -33,16 +36,13 @@ export function sessionMismatchError(input: {
|
|
|
33
36
|
const ownerSession = input.expectedSession ?? "none";
|
|
34
37
|
const currentSession = input.currentSession ?? "none";
|
|
35
38
|
const actor = input.run ? `run:${input.run}` : (input.target ?? "session");
|
|
36
|
-
const hintTarget = input.expectedSession
|
|
37
|
-
? `session:${input.expectedSession}`
|
|
38
|
-
: "session:all";
|
|
39
39
|
return Object.assign(
|
|
40
40
|
new Error(
|
|
41
|
-
`${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=
|
|
41
|
+
`${actor} reason=session_mismatch owner_session=${ownerSession} current_session=${currentSession} hint=inspect_runtime_runs`,
|
|
42
42
|
),
|
|
43
43
|
{
|
|
44
44
|
current_session: input.currentSession,
|
|
45
|
-
hint:
|
|
45
|
+
hint: "inspect target=runtime view=runs",
|
|
46
46
|
owner_session: input.expectedSession,
|
|
47
47
|
reason: "session_mismatch",
|
|
48
48
|
run: input.run,
|
|
@@ -51,18 +51,32 @@ export function sessionMismatchError(input: {
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
export function
|
|
54
|
+
export function assertRunStatusAccessibleToContext(
|
|
55
55
|
runId: string,
|
|
56
|
+
status: Record<string, unknown>,
|
|
56
57
|
ctx: unknown,
|
|
57
58
|
): Record<string, unknown> {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
|
|
59
|
+
const sessionId = requireContextSessionId(ctx, `run:${runId}`);
|
|
60
|
+
const ownerId = typeof status.ownerId === "string" && status.ownerId
|
|
61
|
+
? status.ownerId
|
|
62
|
+
: undefined;
|
|
63
|
+
if (ownerId !== sessionId) {
|
|
61
64
|
throw sessionMismatchError({
|
|
62
65
|
currentSession: sessionId,
|
|
63
|
-
expectedSession:
|
|
66
|
+
expectedSession: ownerId,
|
|
64
67
|
run: runId,
|
|
65
68
|
});
|
|
66
69
|
}
|
|
67
70
|
return status;
|
|
68
71
|
}
|
|
72
|
+
|
|
73
|
+
export function assertRunAccessibleToContext(
|
|
74
|
+
runId: string,
|
|
75
|
+
ctx: unknown,
|
|
76
|
+
): Record<string, unknown> {
|
|
77
|
+
return assertRunStatusAccessibleToContext(
|
|
78
|
+
runId,
|
|
79
|
+
AsyncRuns.getRunStatus(runId),
|
|
80
|
+
ctx,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -42,7 +42,7 @@ export interface InspectToolDeps<TContext = unknown> {
|
|
|
42
42
|
|
|
43
43
|
function runtimeStatus(): Record<string, unknown> {
|
|
44
44
|
return {
|
|
45
|
-
automatic_review:
|
|
45
|
+
automatic_review: Paths.isAutomaticRecipeReviewEnabled(),
|
|
46
46
|
run_root: Paths.getRunStateRoot(),
|
|
47
47
|
state_schema: RuntimeIdentity.RUN_STATE_SCHEMA,
|
|
48
48
|
version: RuntimeIdentity.getPackageVersion(),
|
|
@@ -54,7 +54,7 @@ function runtimeRuns(
|
|
|
54
54
|
deps: InspectToolDeps,
|
|
55
55
|
status: string | undefined,
|
|
56
56
|
): Record<string, unknown> {
|
|
57
|
-
const session = ToolsAccess.
|
|
57
|
+
const session = ToolsAccess.requireContextSessionId(ctx, "runtime Run inventory");
|
|
58
58
|
const listed = deps.listRuns
|
|
59
59
|
? deps.listRuns()
|
|
60
60
|
: AsyncRuns.listRuns(undefined, status);
|
|
@@ -68,8 +68,8 @@ function runtimeRuns(
|
|
|
68
68
|
return run;
|
|
69
69
|
}
|
|
70
70
|
})
|
|
71
|
-
.filter((run) =>
|
|
72
|
-
return {
|
|
71
|
+
.filter((run) => run.ownerId === session);
|
|
72
|
+
return { owner_session: session, runs };
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
function runtimeTriage(
|
|
@@ -463,7 +463,11 @@ function inspectRun(
|
|
|
463
463
|
throw new Error("inspect run:<id> supports view=recipe, view=trace, or view=control.");
|
|
464
464
|
}
|
|
465
465
|
const status = deps.getRunStatus
|
|
466
|
-
?
|
|
466
|
+
? ToolsAccess.assertRunStatusAccessibleToContext(
|
|
467
|
+
run,
|
|
468
|
+
deps.getRunStatus(run),
|
|
469
|
+
ctx,
|
|
470
|
+
)
|
|
467
471
|
: ToolsAccess.assertRunAccessibleToContext(run, ctx);
|
|
468
472
|
const stateDir = String(status.state_dir);
|
|
469
473
|
if (view === "recipe") {
|
|
@@ -46,6 +46,7 @@ function runNextActions(run: string, result: Record<string, unknown>): string[]
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export interface ControlToolDeps {
|
|
49
|
+
getRunStatus?: (run: string) => Record<string, unknown>;
|
|
49
50
|
handleRuntimeControl?: (
|
|
50
51
|
action: string,
|
|
51
52
|
input: unknown,
|
|
@@ -94,7 +95,13 @@ export function createControlToolDefinition<TContext = unknown>(
|
|
|
94
95
|
result = deps.handleRuntimeControl(request.action, request.input);
|
|
95
96
|
} else {
|
|
96
97
|
const run = request.target.slice(4);
|
|
97
|
-
const status =
|
|
98
|
+
const status = deps.getRunStatus
|
|
99
|
+
? ToolsAccess.assertRunStatusAccessibleToContext(
|
|
100
|
+
run,
|
|
101
|
+
deps.getRunStatus(run),
|
|
102
|
+
ctx,
|
|
103
|
+
)
|
|
104
|
+
: ToolsAccess.assertRunAccessibleToContext(run, ctx);
|
|
98
105
|
const runInstanceId =
|
|
99
106
|
typeof status.run_instance_id === "string"
|
|
100
107
|
? status.run_instance_id
|
|
@@ -81,7 +81,7 @@ Use the relevant local skill before non-trivial work in its domain. Keep skill o
|
|
|
81
81
|
- A forwarding delivery id is stable across registration replacement and derives from envelope kind, source `update_id`, and stable recipient binding. Runtime instance and registration generation remain separate attempt fences. Persisted message ownership carries the stable binding so replay can rebind only to its current authenticated registration.
|
|
82
82
|
- A queued receipt persists its acquiring runtime instance, OS pid/process-birth identity, session generation, acquisition id, and acquisition time. Only exact authority may settle or discard it. Same-process session replacement may reconstruct the claim and the original process may settle after transport ownership moves; a foreign process may neither replay nor settle it through generic removal or a copied acquisition id.
|
|
83
83
|
- Startup and elapsed time are not owner-death proof; queued authority has no time lease. Dead-owner recovery groups the complete receipt and transactionally rechecks pid liveness plus process-birth identity: only an absent PID or mismatched stable Linux/macOS birth proof returns all sources to `pending`; a matching proof is `alive`, while Windows or inaccessible birth metadata is `unverifiable`, and both non-dead outcomes keep authority queued. The live-transfer contract is authenticated offer → exact-generation bounded payload staging → recipient CAS acceptance → exact receipt-and-owner ACK → donor removal → recipient readiness. The offer freezes donor settlement/recovery; controls rebuild local closures; negative/mismatched pre-acceptance ACK cancels only an unaccepted offer and retains donor work; a lost post-acceptance ACK cannot cancel recipient authority and leaves donor memory frozen for explicit reconciliation.
|
|
84
|
-
- Execution failures persist bounded diagnostics and attempt state as `retry-wait`. Automatic retry continues indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at 60 seconds; later independent updates continue draining, durable authority is never silently discarded, and legacy `failed` entries resume automatically at startup. Snapshot-plus-segment journals compact only after 256 unapplied revisions or 4 MiB; snapshot-first cleanup tolerates redundant segments, and empty authority may atomically rebind bot/profile identity.
|
|
84
|
+
- Execution failures persist bounded diagnostics and attempt state as `retry-wait`. Automatic retry continues indefinitely with exponential `1s → 2s → 4s → 8s → 16s → 32s → 60s` delay capped at 60 seconds; later independent updates continue draining, durable authority is never silently discarded, and legacy `failed` entries resume automatically at startup. Snapshot-plus-segment journals compact only after 256 unapplied revisions or 4 MiB; snapshot-first cleanup tolerates redundant segments, and empty authority may atomically rebind bot/profile identity. Missing snapshots left by the retired broad temp cleanup rebuild only from a complete provably empty segment chain, while revisionless snapshots may recover from a validated later segment predecessor; otherwise the snapshot and segments move atomically under `tmp/telegram/recovery/` before a fresh journal is published and startup continues with informational recovery evidence.
|
|
85
85
|
- An unresolved reaction delays only the exact governed queue item identified by chat/message sources, not unrelated queue work. Queue receipt publication follows in-memory append and precedes dispatch request; receipt-bearing turns remain queued until every exact source commits.
|
|
86
86
|
- The detailed implementation and release gates live in [`docs/architecture.md`](./docs/architecture.md), [`docs/multi-instance-bus.md`](./docs/multi-instance-bus.md), and [`BACKLOG.md`](./BACKLOG.md).
|
|
87
87
|
|
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## 0.36.10: Transport-Fenced Typing Hotfix
|
|
6
|
+
|
|
7
|
+
- `Typing Authority Fence`: Starts and continues Telegram typing activity only while the Pi instance has direct ownership or a live follower registration, stopping quietly when authority disappears so classic takeover cannot re-arm a stale loop or flood diagnostics with expected follower-registration errors.
|
|
8
|
+
|
|
9
|
+
## 0.36.9: Telegram Status And Generated Controls Hotfix
|
|
10
|
+
|
|
11
|
+
- `Classic Takeover Status`: Keeps a classic-mode client visibly `disconnected` after another Pi instance takes Telegram ownership, preventing stale transport-side activity errors from overriding the authoritative connection state.
|
|
12
|
+
- `Generated Button Surfaces`: Makes vertical full-width controls the phone-readable default, earns multi-column rows only for compact labels or emoji-only spatial controls, encourages concise semantic labels, and requires safe 2–6 button controls whenever a Telegram reply asks a bounded blocking confirmation or choice instead of leaving an avoidable prose-only feedback step.
|
|
13
|
+
|
|
14
|
+
## 0.36.8: Durable Journal Recovery
|
|
15
|
+
|
|
16
|
+
- `Durable Journal Recovery`: Restricts age cleanup to UUID-prefixed downloads, repairs missing or revisionless snapshots from validated segment evidence, and otherwise quarantines the snapshot plus segments before publishing a fresh journal, keeping `/telegram-connect` operational with informational recovery evidence instead of manual JSON repair.
|
|
17
|
+
|
|
5
18
|
## 0.36.7: Context-Aware Proactive Controls
|
|
6
19
|
|
|
7
20
|
- `Proactive Prompt Buttons`: Plans valid top-level `telegram_button` comments before local/autonomous assistant segments enter Rich or HTML proactive delivery, preserving the ordinary callback store and renderer path instead of silently hiding correct controls.
|