@llblab/pi-kit 0.1.13 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -0
- package/CHANGELOG.md +10 -0
- package/README.md +2 -2
- package/node_modules/@llblab/pi-actors/AGENTS.md +5 -3
- package/node_modules/@llblab/pi-actors/BACKLOG.md +1 -1
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +15 -0
- package/node_modules/@llblab/pi-actors/README.md +5 -3
- package/node_modules/@llblab/pi-actors/dist/index.js +4 -1
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +2 -2
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +42 -19
- package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.d.ts +2 -1
- package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +7 -2
- package/node_modules/@llblab/pi-actors/dist/lib/limits.d.ts +9 -0
- package/node_modules/@llblab/pi-actors/dist/lib/limits.js +9 -0
- package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +10 -11
- package/node_modules/@llblab/pi-actors/dist/lib/observability.js +81 -56
- package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +31 -0
- package/node_modules/@llblab/pi-actors/dist/lib/pi.js +180 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +115 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +623 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.d.ts +3 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +341 -13
- package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.d.ts +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.js +5 -3
- package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.d.ts +16 -0
- package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.js +143 -0
- package/node_modules/@llblab/pi-actors/dist/lib/temp.js +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.js +3 -1
- package/node_modules/@llblab/pi-actors/dist/scripts/async-runner.mjs +5 -19
- package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +2 -2
- package/node_modules/@llblab/pi-actors/dist/skills/actors/references/runs.md +1 -1
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/SKILL.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/README.md +1 -0
- package/node_modules/@llblab/pi-actors/docs/async-runs.md +8 -4
- package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +207 -0
- package/node_modules/@llblab/pi-actors/index.ts +4 -1
- package/node_modules/@llblab/pi-actors/lib/async-runs.ts +42 -21
- package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +8 -3
- package/node_modules/@llblab/pi-actors/lib/limits.ts +9 -0
- package/node_modules/@llblab/pi-actors/lib/observability.ts +97 -78
- package/node_modules/@llblab/pi-actors/lib/pi.ts +210 -0
- package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +800 -0
- package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +370 -18
- package/node_modules/@llblab/pi-actors/lib/runs-trace.ts +6 -4
- package/node_modules/@llblab/pi-actors/lib/session-evidence.ts +153 -0
- package/node_modules/@llblab/pi-actors/lib/temp.ts +1 -1
- package/node_modules/@llblab/pi-actors/lib/tools-inspect.ts +4 -1
- package/node_modules/@llblab/pi-actors/package.json +3 -3
- package/node_modules/@llblab/pi-actors/scripts/async-runner.mjs +5 -19
- package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +2 -2
- package/node_modules/@llblab/pi-actors/skills/actors/references/runs.md +1 -1
- package/node_modules/@llblab/pi-actors/skills/swarm/SKILL.md +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +4 -4
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +25 -0
- package/node_modules/@llblab/pi-telegram/README.md +4 -4
- package/node_modules/@llblab/pi-telegram/docs/activity.md +4 -4
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +10 -10
- package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +16 -14
- package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +13 -22
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +6 -7
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +4 -5
- package/node_modules/@llblab/pi-telegram/docs/voice.md +12 -2
- package/node_modules/@llblab/pi-telegram/index.ts +0 -1
- package/node_modules/@llblab/pi-telegram/lib/activity.ts +10 -5
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +16 -17
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +16 -6
- package/node_modules/@llblab/pi-telegram/lib/config.ts +25 -25
- package/node_modules/@llblab/pi-telegram/lib/media.ts +12 -2
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +10 -73
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +9 -8
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +212 -82
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +78 -36
- package/node_modules/@llblab/pi-telegram/lib/status.ts +23 -4
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +10 -1
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1 -1
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +61 -205
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/references/capability-adapters.md +27 -0
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/references/layout-and-state.md +35 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +70 -110
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/configuration.md +15 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +14 -0
- package/package.json +3 -3
|
@@ -5,12 +5,16 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import * as AsyncRuns from "./async-runs.ts";
|
|
8
|
+
import * as Limits from "./limits.ts";
|
|
8
9
|
import * as Observability from "./observability.ts";
|
|
9
10
|
import * as Paths from "./paths.ts";
|
|
10
11
|
import * as Pi from "./pi.ts";
|
|
12
|
+
import * as RunDelivery from "./run-delivery.ts";
|
|
11
13
|
|
|
12
14
|
export interface RunUiRuntime {
|
|
13
15
|
close(): void;
|
|
16
|
+
flushCompletionBatch(ctx: Pi.ExtensionContext): boolean;
|
|
17
|
+
projectContext(messages: unknown[], ctx: Pi.ExtensionContext): unknown[];
|
|
14
18
|
shutdown(
|
|
15
19
|
eventReason: string,
|
|
16
20
|
ownerId: string | undefined,
|
|
@@ -24,6 +28,7 @@ export interface RunUiRuntimeDeps {
|
|
|
24
28
|
createRunStateWatcher?: typeof Observability.createRunStateWatcher;
|
|
25
29
|
createRunTerminalReconciliationLoop?:
|
|
26
30
|
typeof Observability.createRunTerminalReconciliationLoop;
|
|
31
|
+
deliveryDebounceMs?: number;
|
|
27
32
|
getActiveContext(): Pi.ExtensionContext | undefined;
|
|
28
33
|
notificationDelayMs?: number;
|
|
29
34
|
onCallbackError?: (error: unknown) => void;
|
|
@@ -36,12 +41,16 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
36
41
|
let activeContext: Pi.ExtensionContext | undefined;
|
|
37
42
|
let activeOwnerId: string | undefined;
|
|
38
43
|
let animationInterval: NodeJS.Timeout | undefined;
|
|
44
|
+
let deliveryTimeout: NodeJS.Timeout | undefined;
|
|
39
45
|
let notifyTimeout: NodeJS.Timeout | undefined;
|
|
46
|
+
let recoverQueuedBatch = false;
|
|
47
|
+
let recoverQueuedSteers = false;
|
|
40
48
|
let running = false;
|
|
41
49
|
let lastWatcherDiagnosticId = 0;
|
|
42
50
|
const observation = Observability.createRunUiObservationState();
|
|
51
|
+
const deliveryRecoveryDiagnostics = new Set<string>();
|
|
43
52
|
const retirementAttempts = new Set<string>();
|
|
44
|
-
const
|
|
53
|
+
const steerDiagnostics = new Set<string>();
|
|
45
54
|
|
|
46
55
|
const close = (): void => {
|
|
47
56
|
running = false;
|
|
@@ -59,6 +68,12 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
59
68
|
}
|
|
60
69
|
if (notifyTimeout) clearTimeout(notifyTimeout);
|
|
61
70
|
notifyTimeout = undefined;
|
|
71
|
+
if (deliveryTimeout) clearTimeout(deliveryTimeout);
|
|
72
|
+
deliveryTimeout = undefined;
|
|
73
|
+
recoverQueuedBatch = false;
|
|
74
|
+
recoverQueuedSteers = false;
|
|
75
|
+
deliveryRecoveryDiagnostics.clear();
|
|
76
|
+
steerDiagnostics.clear();
|
|
62
77
|
if (animationInterval) clearInterval(animationInterval);
|
|
63
78
|
animationInterval = undefined;
|
|
64
79
|
};
|
|
@@ -107,29 +122,235 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
107
122
|
stopAfterCallbackFailure("Run retirement callback", error, ctx),
|
|
108
123
|
);
|
|
109
124
|
};
|
|
125
|
+
const journal = (ownerId: string): RunDelivery.RunDeliveryJournal =>
|
|
126
|
+
RunDelivery.readRunDeliveryJournal(
|
|
127
|
+
Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
128
|
+
ownerId,
|
|
129
|
+
);
|
|
130
|
+
const finishPresentedBatch = (
|
|
131
|
+
ownerId: string,
|
|
132
|
+
batch: RunDelivery.RunCompletionBatch,
|
|
133
|
+
): boolean => {
|
|
134
|
+
if (batch.phase !== "presented") return false;
|
|
135
|
+
for (const member of batch.members) {
|
|
136
|
+
AsyncRuns.markRunTerminalNotificationHandled(
|
|
137
|
+
member.state_dir,
|
|
138
|
+
member.status,
|
|
139
|
+
member.run_instance_id,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return RunDelivery.finalizeRunCompletionBatch({
|
|
143
|
+
batchId: batch.batch_id,
|
|
144
|
+
ownerId,
|
|
145
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
const recoverPresentedBatch = (ownerId: string): void => {
|
|
149
|
+
const batch = journal(ownerId).completion_batch;
|
|
150
|
+
if (batch?.phase === "presented") finishPresentedBatch(ownerId, batch);
|
|
151
|
+
};
|
|
152
|
+
const finishPresentedSteer = (
|
|
153
|
+
ownerId: string,
|
|
154
|
+
steer: RunDelivery.RunSteerEnvelope,
|
|
155
|
+
): boolean => {
|
|
156
|
+
if (steer.phase !== "presented") return false;
|
|
157
|
+
AsyncRuns.markRunSteerPresentationHandled(
|
|
158
|
+
steer.state_dir,
|
|
159
|
+
steer.run_instance_id,
|
|
160
|
+
steer.event_id,
|
|
161
|
+
steer.steer_id,
|
|
162
|
+
);
|
|
163
|
+
return RunDelivery.finalizeRunSteer({
|
|
164
|
+
ownerId,
|
|
165
|
+
steerId: steer.steer_id,
|
|
166
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
const recoverPresentedSteers = (ownerId: string): void => {
|
|
170
|
+
for (const steer of journal(ownerId).steers) {
|
|
171
|
+
if (steer.phase === "presented") finishPresentedSteer(ownerId, steer);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
const boundedSteerContent = (event: Observability.RunAttentionEvent): string => {
|
|
175
|
+
const text = Observability.formatRunAttentionMessage(event);
|
|
176
|
+
if (Buffer.byteLength(text, "utf8") <= Limits.RUN_DELIVERY_STEER_MAX_BYTES) {
|
|
177
|
+
return text;
|
|
178
|
+
}
|
|
179
|
+
let end = Math.min(text.length, Limits.RUN_DELIVERY_STEER_MAX_BYTES - 3);
|
|
180
|
+
while (end > 0 && Buffer.byteLength(text.slice(0, end), "utf8") >
|
|
181
|
+
Limits.RUN_DELIVERY_STEER_MAX_BYTES - 3) end -= 1;
|
|
182
|
+
return `${text.slice(0, end)}…`;
|
|
183
|
+
};
|
|
184
|
+
const admitSteerEvents = (
|
|
185
|
+
ctx: Pi.ExtensionContext,
|
|
186
|
+
ownerId: string,
|
|
187
|
+
events: Observability.RunAttentionEvent[],
|
|
188
|
+
): void => {
|
|
189
|
+
for (const event of events.filter(Observability.isRunSteerAttentionEvent)) {
|
|
190
|
+
if (!event.runInstanceId) {
|
|
191
|
+
Observability.retryRunAttentionEvent(observation, event);
|
|
192
|
+
const key = `${event.stateDir}:${event.id}:missing_generation`;
|
|
193
|
+
if (!steerDiagnostics.has(key)) {
|
|
194
|
+
steerDiagnostics.add(key);
|
|
195
|
+
ctx.ui.notify(
|
|
196
|
+
`Actor urgent steer ${event.id} remains retryable because its Run generation is unavailable.`,
|
|
197
|
+
"warning",
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
RunDelivery.admitRunSteerEnvelope({
|
|
204
|
+
content: boundedSteerContent(event),
|
|
205
|
+
eventId: event.id,
|
|
206
|
+
kind: event.kind,
|
|
207
|
+
level: event.level,
|
|
208
|
+
occurredAt: event.ts,
|
|
209
|
+
ownerId,
|
|
210
|
+
run: event.run,
|
|
211
|
+
runInstanceId: event.runInstanceId,
|
|
212
|
+
stateDir: event.stateDir,
|
|
213
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
214
|
+
});
|
|
215
|
+
} catch (error) {
|
|
216
|
+
Observability.retryRunAttentionEvent(observation, event);
|
|
217
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
218
|
+
const key = `${event.stateDir}:${event.id}:${message}`;
|
|
219
|
+
if (!steerDiagnostics.has(key)) {
|
|
220
|
+
steerDiagnostics.add(key);
|
|
221
|
+
ctx.ui.notify(
|
|
222
|
+
`Actor urgent steer ${event.id} remains retryable: ${message.replaceAll(/\s+/g, " ").slice(0, 240)}`,
|
|
223
|
+
"warning",
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const admitCompletionTransitions = (
|
|
230
|
+
ownerId: string,
|
|
231
|
+
transitions: Observability.RunTransition[],
|
|
232
|
+
): boolean => {
|
|
233
|
+
const existing = journal(ownerId).completion_batch;
|
|
234
|
+
if (existing) return true;
|
|
235
|
+
const members = Observability.collectRunCompletionBatchMembers(transitions)
|
|
236
|
+
.slice(0, Limits.RUN_DELIVERY_BATCH_MAX_MEMBERS);
|
|
237
|
+
if (members.length === 0) return false;
|
|
238
|
+
RunDelivery.admitRunCompletionBatch({
|
|
239
|
+
members,
|
|
240
|
+
ownerId,
|
|
241
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
242
|
+
});
|
|
243
|
+
return true;
|
|
244
|
+
};
|
|
245
|
+
const isIdle = (ctx: Pi.ExtensionContext): boolean =>
|
|
246
|
+
typeof ctx.isIdle !== "function" || ctx.isIdle();
|
|
247
|
+
let flushCompletionBatch = (_ctx: Pi.ExtensionContext): boolean => false;
|
|
248
|
+
const flushSteers = (ctx: Pi.ExtensionContext, ownerId: string): boolean => {
|
|
249
|
+
const recovering = recoverQueuedSteers;
|
|
250
|
+
for (const steer of journal(ownerId).steers) {
|
|
251
|
+
if (steer.phase === "presented") {
|
|
252
|
+
finishPresentedSteer(ownerId, steer);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
let phase = steer.phase;
|
|
256
|
+
if (phase === "queued") {
|
|
257
|
+
if (!recovering) continue;
|
|
258
|
+
const evidence = Pi.inspectRunSteerSessionEvidence(ctx, {
|
|
259
|
+
content: steer.content,
|
|
260
|
+
eventId: steer.event_id,
|
|
261
|
+
steerId: steer.steer_id,
|
|
262
|
+
});
|
|
263
|
+
if (evidence.status === "present") continue;
|
|
264
|
+
if (evidence.status !== "absent") {
|
|
265
|
+
const key = `${steer.steer_id}:${evidence.status}:${evidence.reason ?? ""}`;
|
|
266
|
+
if (!steerDiagnostics.has(key)) {
|
|
267
|
+
steerDiagnostics.add(key);
|
|
268
|
+
ctx.ui.notify(
|
|
269
|
+
`Actor urgent steer recovery is ${evidence.status}: ${evidence.reason ?? "conflicting session evidence"}. Event ${steer.event_id} remains queued.`,
|
|
270
|
+
"warning",
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (!RunDelivery.resetRunSteerPending({
|
|
276
|
+
ownerId,
|
|
277
|
+
steerId: steer.steer_id,
|
|
278
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
279
|
+
})) continue;
|
|
280
|
+
phase = "pending";
|
|
281
|
+
}
|
|
282
|
+
if (phase !== "pending") continue;
|
|
283
|
+
try {
|
|
284
|
+
Pi.sendRunSteer(deps.pi, {
|
|
285
|
+
content: steer.content,
|
|
286
|
+
eventId: steer.event_id,
|
|
287
|
+
steerId: steer.steer_id,
|
|
288
|
+
});
|
|
289
|
+
} catch (error) {
|
|
290
|
+
RunDelivery.recordRunSteerDeliveryFailure({
|
|
291
|
+
error,
|
|
292
|
+
ownerId,
|
|
293
|
+
steerId: steer.steer_id,
|
|
294
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
295
|
+
});
|
|
296
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
297
|
+
ctx.ui.notify(
|
|
298
|
+
`Actor urgent steer delivery failed for event ${steer.event_id}: ${message.replaceAll(/\s+/g, " ").slice(0, 240)}`,
|
|
299
|
+
"error",
|
|
300
|
+
);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
RunDelivery.markRunSteerQueued({
|
|
304
|
+
ownerId,
|
|
305
|
+
steerId: steer.steer_id,
|
|
306
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
recoverQueuedSteers = false;
|
|
310
|
+
return journal(ownerId).steers.length > 0;
|
|
311
|
+
};
|
|
312
|
+
const scheduleCompletionFlush = (): void => {
|
|
313
|
+
if (!running) return;
|
|
314
|
+
if (deliveryTimeout) clearTimeout(deliveryTimeout);
|
|
315
|
+
deliveryTimeout = setTimeout(() => {
|
|
316
|
+
deliveryTimeout = undefined;
|
|
317
|
+
runActiveCallback("completion delivery callback", (ctx) => {
|
|
318
|
+
flushCompletionBatch(ctx);
|
|
319
|
+
});
|
|
320
|
+
}, deps.deliveryDebounceMs ?? 100);
|
|
321
|
+
deliveryTimeout.unref?.();
|
|
322
|
+
};
|
|
110
323
|
const update = (
|
|
111
324
|
ctx: Pi.ExtensionContext,
|
|
112
325
|
ownerId: string,
|
|
113
326
|
notify = false,
|
|
114
327
|
terminalOnly = false,
|
|
115
|
-
):
|
|
328
|
+
): boolean => {
|
|
116
329
|
const snapshot = Observability.readRunUiSnapshot(observation, ownerId);
|
|
117
330
|
ctx.ui.setStatus(
|
|
118
331
|
"zz-pi-actors-runs",
|
|
119
332
|
snapshot.status ? ctx.ui.theme.fg("dim", snapshot.status) : undefined,
|
|
120
333
|
);
|
|
121
|
-
if (!notify) return;
|
|
334
|
+
if (!notify) return false;
|
|
122
335
|
const sink = Pi.createNotificationSink(deps.pi, ctx);
|
|
123
336
|
retireCandidateRuns(ctx, snapshot.summary);
|
|
124
|
-
|
|
125
|
-
snapshot.transitions
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
);
|
|
337
|
+
const hasCompletionCandidates =
|
|
338
|
+
Observability.collectRunCompletionBatchMembers(snapshot.transitions).length > 0;
|
|
339
|
+
const hasCompletionBatch = Boolean(journal(ownerId).completion_batch);
|
|
340
|
+
admitSteerEvents(ctx, ownerId, snapshot.attentionEvents);
|
|
129
341
|
Observability.pruneRunUiObservationState(observation, snapshot);
|
|
130
342
|
if (!terminalOnly) {
|
|
131
|
-
Observability.deliverRunAttentionNotifications(
|
|
343
|
+
Observability.deliverRunAttentionNotifications(
|
|
344
|
+
snapshot.attentionEvents.filter((event) =>
|
|
345
|
+
!Observability.isRunSteerAttentionEvent(event)),
|
|
346
|
+
sink,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
const hasSteers = flushSteers(ctx, ownerId);
|
|
350
|
+
if ((hasCompletionBatch || hasCompletionCandidates) && isIdle(ctx)) {
|
|
351
|
+
scheduleCompletionFlush();
|
|
132
352
|
}
|
|
353
|
+
return hasCompletionBatch || hasCompletionCandidates || hasSteers;
|
|
133
354
|
};
|
|
134
355
|
const reportDiagnostics = (ctx: Pi.ExtensionContext): void => {
|
|
135
356
|
for (const diagnostic of watcher.getDiagnostics()) {
|
|
@@ -147,8 +368,8 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
147
368
|
notifyTimeout = setTimeout(() => {
|
|
148
369
|
runActiveCallback("Run watcher callback", (ctx, ownerId) => {
|
|
149
370
|
watcher.refresh();
|
|
150
|
-
update(ctx, ownerId, true);
|
|
151
|
-
deps.onRunEvent();
|
|
371
|
+
const completionDeferred = update(ctx, ownerId, true);
|
|
372
|
+
if (!completionDeferred) deps.onRunEvent();
|
|
152
373
|
reportDiagnostics(ctx);
|
|
153
374
|
});
|
|
154
375
|
}, deps.notificationDelayMs ?? 50);
|
|
@@ -172,13 +393,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
172
393
|
},
|
|
173
394
|
reconcile: () => {
|
|
174
395
|
runActiveCallback("terminal reconciliation callback", (ctx, ownerId) => {
|
|
175
|
-
|
|
176
|
-
inFlight: terminalNotificationsInFlight,
|
|
177
|
-
ownerId,
|
|
178
|
-
sink: Pi.createNotificationSink(deps.pi, ctx),
|
|
179
|
-
state: observation,
|
|
180
|
-
includeAttention: true,
|
|
181
|
-
});
|
|
396
|
+
update(ctx, ownerId, true);
|
|
182
397
|
reportDiagnostics(ctx);
|
|
183
398
|
});
|
|
184
399
|
},
|
|
@@ -187,8 +402,141 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
187
402
|
},
|
|
188
403
|
});
|
|
189
404
|
|
|
405
|
+
flushCompletionBatch = (ctx: Pi.ExtensionContext): boolean => {
|
|
406
|
+
if (!running || activeContext !== ctx || deps.getActiveContext() !== ctx) return false;
|
|
407
|
+
const ownerId = activeOwnerId;
|
|
408
|
+
if (!ownerId) return false;
|
|
409
|
+
if (flushSteers(ctx, ownerId)) return true;
|
|
410
|
+
let batch = journal(ownerId).completion_batch;
|
|
411
|
+
if (!batch) {
|
|
412
|
+
const snapshot = Observability.readRunUiSnapshot(observation, ownerId);
|
|
413
|
+
admitCompletionTransitions(ownerId, snapshot.transitions);
|
|
414
|
+
Observability.pruneRunUiObservationState(observation, snapshot);
|
|
415
|
+
batch = journal(ownerId).completion_batch;
|
|
416
|
+
}
|
|
417
|
+
if (!batch) return false;
|
|
418
|
+
if (batch.phase === "presented") {
|
|
419
|
+
finishPresentedBatch(ownerId, batch);
|
|
420
|
+
const snapshot = Observability.readRunUiSnapshot(observation, ownerId);
|
|
421
|
+
admitCompletionTransitions(ownerId, snapshot.transitions);
|
|
422
|
+
Observability.pruneRunUiObservationState(observation, snapshot);
|
|
423
|
+
batch = journal(ownerId).completion_batch;
|
|
424
|
+
if (!batch) return false;
|
|
425
|
+
}
|
|
426
|
+
if (!isIdle(ctx)) return true;
|
|
427
|
+
const content = RunDelivery.formatRunCompletionBatchMessage(batch);
|
|
428
|
+
if (batch.phase === "queued") {
|
|
429
|
+
if (!recoverQueuedBatch) return true;
|
|
430
|
+
recoverQueuedBatch = false;
|
|
431
|
+
const evidence = Pi.inspectRunCompletionBatchSessionEvidence(
|
|
432
|
+
ctx,
|
|
433
|
+
batch.batch_id,
|
|
434
|
+
content,
|
|
435
|
+
);
|
|
436
|
+
if (evidence.status === "present") return true;
|
|
437
|
+
if (evidence.status !== "absent") {
|
|
438
|
+
const diagnosticKey = `${batch.batch_id}:${evidence.status}:${evidence.reason ?? ""}`;
|
|
439
|
+
if (!deliveryRecoveryDiagnostics.has(diagnosticKey)) {
|
|
440
|
+
deliveryRecoveryDiagnostics.add(diagnosticKey);
|
|
441
|
+
ctx.ui.notify(
|
|
442
|
+
`Actor completion recovery is ${evidence.status}: ${evidence.reason ?? "conflicting session evidence"}. Batch ${batch.batch_id} remains queued.`,
|
|
443
|
+
"warning",
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
if (!RunDelivery.resetRunCompletionBatchPending({
|
|
449
|
+
batchId: batch.batch_id,
|
|
450
|
+
ownerId,
|
|
451
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
452
|
+
})) return true;
|
|
453
|
+
batch = journal(ownerId).completion_batch!;
|
|
454
|
+
}
|
|
455
|
+
if (!isIdle(ctx)) return true;
|
|
456
|
+
try {
|
|
457
|
+
Pi.sendRunCompletionBatch(deps.pi, batch.batch_id, content);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
RunDelivery.recordRunCompletionBatchDeliveryFailure({
|
|
460
|
+
batchId: batch.batch_id,
|
|
461
|
+
error,
|
|
462
|
+
ownerId,
|
|
463
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
464
|
+
});
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
if (!RunDelivery.markRunCompletionBatchQueued({
|
|
468
|
+
batchId: batch.batch_id,
|
|
469
|
+
ownerId,
|
|
470
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
471
|
+
})) {
|
|
472
|
+
throw new Error("Completion batch changed before queue acknowledgment");
|
|
473
|
+
}
|
|
474
|
+
recoverQueuedBatch = false;
|
|
475
|
+
return true;
|
|
476
|
+
};
|
|
477
|
+
|
|
190
478
|
return {
|
|
191
479
|
close,
|
|
480
|
+
flushCompletionBatch,
|
|
481
|
+
projectContext(messages, ctx) {
|
|
482
|
+
if (!running || activeContext !== ctx || deps.getActiveContext() !== ctx) {
|
|
483
|
+
return messages;
|
|
484
|
+
}
|
|
485
|
+
const ownerId = activeOwnerId;
|
|
486
|
+
if (!ownerId) return messages;
|
|
487
|
+
const steerContext = Pi.dedupeRunSteerContext(messages);
|
|
488
|
+
let contextMessages = steerContext.messages;
|
|
489
|
+
for (const steer of journal(ownerId).steers) {
|
|
490
|
+
const presented = steerContext.steers.get(steer.steer_id);
|
|
491
|
+
if (
|
|
492
|
+
!presented ||
|
|
493
|
+
presented.eventId !== steer.event_id ||
|
|
494
|
+
presented.content !== steer.content
|
|
495
|
+
) {
|
|
496
|
+
contextMessages = Pi.removeRunSteerFromContext(
|
|
497
|
+
contextMessages,
|
|
498
|
+
steer.steer_id,
|
|
499
|
+
);
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
if (!RunDelivery.markRunSteerQueued({
|
|
503
|
+
ownerId,
|
|
504
|
+
steerId: steer.steer_id,
|
|
505
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
506
|
+
})) continue;
|
|
507
|
+
if (!RunDelivery.markRunSteerPresented({
|
|
508
|
+
ownerId,
|
|
509
|
+
steerId: steer.steer_id,
|
|
510
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
511
|
+
})) continue;
|
|
512
|
+
const durable = journal(ownerId).steers.find((item) =>
|
|
513
|
+
item.steer_id === steer.steer_id);
|
|
514
|
+
if (durable) finishPresentedSteer(ownerId, durable);
|
|
515
|
+
}
|
|
516
|
+
const projected = Pi.dedupeRunCompletionBatchContext(contextMessages);
|
|
517
|
+
const batch = journal(ownerId).completion_batch;
|
|
518
|
+
if (!batch) return projected.messages;
|
|
519
|
+
const content = projected.batches.get(batch.batch_id);
|
|
520
|
+
if (content !== RunDelivery.formatRunCompletionBatchMessage(batch)) {
|
|
521
|
+
return Pi.removeRunCompletionBatchFromContext(
|
|
522
|
+
projected.messages,
|
|
523
|
+
batch.batch_id,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
if (!RunDelivery.markRunCompletionBatchQueued({
|
|
527
|
+
batchId: batch.batch_id,
|
|
528
|
+
ownerId,
|
|
529
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
530
|
+
})) return projected.messages;
|
|
531
|
+
if (!RunDelivery.markRunCompletionBatchPresented({
|
|
532
|
+
batchId: batch.batch_id,
|
|
533
|
+
ownerId,
|
|
534
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
535
|
+
})) return projected.messages;
|
|
536
|
+
const presented = journal(ownerId).completion_batch;
|
|
537
|
+
if (presented) finishPresentedBatch(ownerId, presented);
|
|
538
|
+
return projected.messages;
|
|
539
|
+
},
|
|
192
540
|
shutdown(eventReason, ownerId, ctx) {
|
|
193
541
|
if (!ownerId) return;
|
|
194
542
|
const teardown = (
|
|
@@ -212,8 +560,12 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
212
560
|
close();
|
|
213
561
|
activeContext = ctx;
|
|
214
562
|
activeOwnerId = ownerId;
|
|
563
|
+
recoverQueuedBatch = true;
|
|
564
|
+
recoverQueuedSteers = true;
|
|
215
565
|
running = true;
|
|
216
566
|
try {
|
|
567
|
+
recoverPresentedBatch(ownerId);
|
|
568
|
+
recoverPresentedSteers(ownerId);
|
|
217
569
|
Observability.primeRunAttentionState(observation, ownerId);
|
|
218
570
|
update(ctx, ownerId, true, true);
|
|
219
571
|
watcher.refresh();
|
|
@@ -35,7 +35,7 @@ export interface TraceEvent {
|
|
|
35
35
|
summary?: string;
|
|
36
36
|
data?: unknown;
|
|
37
37
|
level?: "info" | "warning" | "error";
|
|
38
|
-
attention?: "notify" | "followup";
|
|
38
|
+
attention?: "notify" | "followup" | "steer";
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export interface AppendTraceEventInput {
|
|
@@ -167,9 +167,10 @@ function normalizeTraceEventInput(input: unknown): AppendTraceEventInput {
|
|
|
167
167
|
if (
|
|
168
168
|
record.attention !== undefined &&
|
|
169
169
|
record.attention !== "notify" &&
|
|
170
|
-
record.attention !== "followup"
|
|
170
|
+
record.attention !== "followup" &&
|
|
171
|
+
record.attention !== "steer"
|
|
171
172
|
) {
|
|
172
|
-
throw new Error("Trace event attention must be notify or
|
|
173
|
+
throw new Error("Trace event attention must be notify, followup, or steer");
|
|
173
174
|
}
|
|
174
175
|
return {
|
|
175
176
|
kind: record.kind,
|
|
@@ -295,7 +296,8 @@ function isTraceEvent(value: unknown): value is TraceEvent {
|
|
|
295
296
|
) return false;
|
|
296
297
|
return record.attention === undefined ||
|
|
297
298
|
record.attention === "notify" ||
|
|
298
|
-
record.attention === "followup"
|
|
299
|
+
record.attention === "followup" ||
|
|
300
|
+
record.attention === "steer";
|
|
299
301
|
}
|
|
300
302
|
|
|
301
303
|
function isCount(value: unknown): value is number {
|
|
@@ -61,7 +61,160 @@ export interface SessionEvidenceReadOptions {
|
|
|
61
61
|
maxTurns?: number;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export type ActiveSessionEntryMatch = "present" | "conflict" | undefined;
|
|
65
|
+
export interface ActiveSessionEntryEvidence {
|
|
66
|
+
examinedBytes: number;
|
|
67
|
+
examinedEntries: number;
|
|
68
|
+
reason?: string;
|
|
69
|
+
status: "present" | "absent" | "conflict" | "unknown";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ActiveSessionEntryEvidenceInput {
|
|
73
|
+
getEntry(id: string): unknown;
|
|
74
|
+
leaf: unknown;
|
|
75
|
+
match(entry: Record<string, unknown>): ActiveSessionEntryMatch;
|
|
76
|
+
maxBytes?: number;
|
|
77
|
+
maxEntries?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
64
80
|
const SENSITIVE_KEY = /(?:^|[_-])(?:api[-_]?key|authorization|cookie|credential|password|private[-_]?key|secret|secret[-_]?access[-_]?key|token)$|^(?:access|refresh|auth|api)Token$|^(?:clientSecret|privateKey|secretAccessKey)$/i;
|
|
81
|
+
|
|
82
|
+
function boundedJsonStringBytes(value: string): number {
|
|
83
|
+
let bytes = Buffer.byteLength(value, "utf8") + 2;
|
|
84
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
85
|
+
const code = value.charCodeAt(index);
|
|
86
|
+
if (code === 0x22 || code === 0x5c) bytes += 1;
|
|
87
|
+
else if (code < 0x20) bytes += [0x08, 0x09, 0x0a, 0x0c, 0x0d].includes(code) ? 1 : 5;
|
|
88
|
+
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
89
|
+
const next = value.charCodeAt(index + 1);
|
|
90
|
+
if (next >= 0xdc00 && next <= 0xdfff) index += 1;
|
|
91
|
+
else bytes += 3;
|
|
92
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) bytes += 3;
|
|
93
|
+
}
|
|
94
|
+
return bytes;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function boundedJsonBytes(
|
|
98
|
+
value: unknown,
|
|
99
|
+
ceiling: number,
|
|
100
|
+
seen = new WeakSet<object>(),
|
|
101
|
+
): number | undefined {
|
|
102
|
+
if (value === null) return 4;
|
|
103
|
+
if (typeof value === "string") return boundedJsonStringBytes(value);
|
|
104
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(value).length : 4;
|
|
105
|
+
if (typeof value === "boolean") return value ? 4 : 5;
|
|
106
|
+
if (typeof value !== "object") return undefined;
|
|
107
|
+
if (seen.has(value)) return undefined;
|
|
108
|
+
seen.add(value);
|
|
109
|
+
let bytes = 2;
|
|
110
|
+
const values: Array<[string | undefined, unknown]> = Array.isArray(value)
|
|
111
|
+
? value.map((item) => [
|
|
112
|
+
undefined,
|
|
113
|
+
item === undefined || typeof item === "function" || typeof item === "symbol"
|
|
114
|
+
? null
|
|
115
|
+
: item,
|
|
116
|
+
])
|
|
117
|
+
: Object.entries(value as Record<string, unknown>).filter(([, item]) =>
|
|
118
|
+
item !== undefined && typeof item !== "function" && typeof item !== "symbol");
|
|
119
|
+
for (const [key, item] of values) {
|
|
120
|
+
if (bytes > ceiling) break;
|
|
121
|
+
if (bytes > 2) bytes += 1;
|
|
122
|
+
if (key !== undefined) bytes += boundedJsonStringBytes(key) + 1;
|
|
123
|
+
const itemBytes = boundedJsonBytes(item, ceiling - bytes, seen);
|
|
124
|
+
if (itemBytes === undefined) {
|
|
125
|
+
seen.delete(value);
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
bytes += itemBytes;
|
|
129
|
+
}
|
|
130
|
+
seen.delete(value);
|
|
131
|
+
return bytes;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Walk only the active parent chain under exact entry and byte ceilings. */
|
|
135
|
+
export function inspectBoundedActiveSessionEntries(
|
|
136
|
+
input: ActiveSessionEntryEvidenceInput,
|
|
137
|
+
): ActiveSessionEntryEvidence {
|
|
138
|
+
const maxBytes = input.maxBytes ?? Limits.RUN_DELIVERY_SESSION_MAX_BYTES;
|
|
139
|
+
const maxEntries = input.maxEntries ?? Limits.RUN_DELIVERY_SESSION_MAX_ENTRIES;
|
|
140
|
+
if (input.leaf === undefined || input.leaf === null) {
|
|
141
|
+
return { examinedBytes: 0, examinedEntries: 0, status: "absent" };
|
|
142
|
+
}
|
|
143
|
+
const visited = new Set<string>();
|
|
144
|
+
let current: unknown = input.leaf;
|
|
145
|
+
let examinedBytes = 0;
|
|
146
|
+
let examinedEntries = 0;
|
|
147
|
+
while (current !== undefined) {
|
|
148
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
149
|
+
return {
|
|
150
|
+
examinedBytes,
|
|
151
|
+
examinedEntries,
|
|
152
|
+
reason: "active session entry is malformed",
|
|
153
|
+
status: "unknown",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const entry = current as Record<string, unknown>;
|
|
157
|
+
if (typeof entry.id !== "string" || !entry.id) {
|
|
158
|
+
return {
|
|
159
|
+
examinedBytes,
|
|
160
|
+
examinedEntries,
|
|
161
|
+
reason: "active session entry id is malformed",
|
|
162
|
+
status: "unknown",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (visited.has(entry.id)) {
|
|
166
|
+
return {
|
|
167
|
+
examinedBytes,
|
|
168
|
+
examinedEntries,
|
|
169
|
+
reason: "active session entry cycle detected",
|
|
170
|
+
status: "unknown",
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
visited.add(entry.id);
|
|
174
|
+
const bytes = boundedJsonBytes(entry, maxBytes - examinedBytes);
|
|
175
|
+
if (bytes === undefined) {
|
|
176
|
+
return {
|
|
177
|
+
examinedBytes,
|
|
178
|
+
examinedEntries,
|
|
179
|
+
reason: "active session entry is not serializable",
|
|
180
|
+
status: "unknown",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (examinedEntries >= maxEntries || examinedBytes + bytes > maxBytes) {
|
|
184
|
+
return {
|
|
185
|
+
examinedBytes,
|
|
186
|
+
examinedEntries,
|
|
187
|
+
reason: "active session evidence exceeds its inspection bound",
|
|
188
|
+
status: "unknown",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
examinedEntries += 1;
|
|
192
|
+
examinedBytes += bytes;
|
|
193
|
+
const matched = input.match(entry);
|
|
194
|
+
if (matched) return { examinedBytes, examinedEntries, status: matched };
|
|
195
|
+
if (entry.parentId === null) {
|
|
196
|
+
return { examinedBytes, examinedEntries, status: "absent" };
|
|
197
|
+
}
|
|
198
|
+
if (typeof entry.parentId !== "string" || !entry.parentId) {
|
|
199
|
+
return {
|
|
200
|
+
examinedBytes,
|
|
201
|
+
examinedEntries,
|
|
202
|
+
reason: "active session parent is malformed",
|
|
203
|
+
status: "unknown",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
current = input.getEntry(entry.parentId);
|
|
207
|
+
if (current === undefined) {
|
|
208
|
+
return {
|
|
209
|
+
examinedBytes,
|
|
210
|
+
examinedEntries,
|
|
211
|
+
reason: "active session parent is unavailable",
|
|
212
|
+
status: "unknown",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { examinedBytes, examinedEntries, status: "unknown" };
|
|
217
|
+
}
|
|
65
218
|
const SENSITIVE_TEXT = /(bearer\s+)[A-Za-z0-9._~+/=-]+|["']?\b(api[-_]?key|authorization|clientSecret|cookie|password|private[-_]?key|privateKey|secret|secretAccessKey|token)["']?(\s*[:=]\s*)["']?([^\s,;"'}]+)/gi;
|
|
66
219
|
|
|
67
220
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
@@ -14,7 +14,7 @@ export async function cleanupStaleTempEntries(
|
|
|
14
14
|
tempDir: string,
|
|
15
15
|
maxAgeMs = DEFAULT_TEMP_MAX_AGE_MS,
|
|
16
16
|
now = Date.now(),
|
|
17
|
-
preservedEntries = new Set(["runs"]),
|
|
17
|
+
preservedEntries = new Set(["runs", "delivery"]),
|
|
18
18
|
): Promise<number> {
|
|
19
19
|
let entries: Array<{ name: string }>;
|
|
20
20
|
let removed = 0;
|
|
@@ -122,7 +122,10 @@ function runtimeTriage(
|
|
|
122
122
|
const stateDir = typeof run.state_dir === "string" ? run.state_dir : undefined;
|
|
123
123
|
if (!stateDir) return [];
|
|
124
124
|
return RunsTrace.readRunTraceEvents(stateDir)
|
|
125
|
-
.filter((event) =>
|
|
125
|
+
.filter((event) =>
|
|
126
|
+
event.attention === "notify" ||
|
|
127
|
+
event.attention === "followup" ||
|
|
128
|
+
event.attention === "steer")
|
|
126
129
|
.map((event) => ({
|
|
127
130
|
run: run.run,
|
|
128
131
|
id: event.id,
|