@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
|
@@ -4,19 +4,25 @@
|
|
|
4
4
|
* Owns event-driven run UI coordination without owning actor execution semantics.
|
|
5
5
|
*/
|
|
6
6
|
import * as AsyncRuns from "./async-runs.js";
|
|
7
|
+
import * as Limits from "./limits.js";
|
|
7
8
|
import * as Observability from "./observability.js";
|
|
8
9
|
import * as Paths from "./paths.js";
|
|
9
10
|
import * as Pi from "./pi.js";
|
|
11
|
+
import * as RunDelivery from "./run-delivery.js";
|
|
10
12
|
export function createRunUiRuntime(deps) {
|
|
11
13
|
let activeContext;
|
|
12
14
|
let activeOwnerId;
|
|
13
15
|
let animationInterval;
|
|
16
|
+
let deliveryTimeout;
|
|
14
17
|
let notifyTimeout;
|
|
18
|
+
let recoverQueuedBatch = false;
|
|
19
|
+
let recoverQueuedSteers = false;
|
|
15
20
|
let running = false;
|
|
16
21
|
let lastWatcherDiagnosticId = 0;
|
|
17
22
|
const observation = Observability.createRunUiObservationState();
|
|
23
|
+
const deliveryRecoveryDiagnostics = new Set();
|
|
18
24
|
const retirementAttempts = new Set();
|
|
19
|
-
const
|
|
25
|
+
const steerDiagnostics = new Set();
|
|
20
26
|
const close = () => {
|
|
21
27
|
running = false;
|
|
22
28
|
activeContext = undefined;
|
|
@@ -36,6 +42,13 @@ export function createRunUiRuntime(deps) {
|
|
|
36
42
|
if (notifyTimeout)
|
|
37
43
|
clearTimeout(notifyTimeout);
|
|
38
44
|
notifyTimeout = undefined;
|
|
45
|
+
if (deliveryTimeout)
|
|
46
|
+
clearTimeout(deliveryTimeout);
|
|
47
|
+
deliveryTimeout = undefined;
|
|
48
|
+
recoverQueuedBatch = false;
|
|
49
|
+
recoverQueuedSteers = false;
|
|
50
|
+
deliveryRecoveryDiagnostics.clear();
|
|
51
|
+
steerDiagnostics.clear();
|
|
39
52
|
if (animationInterval)
|
|
40
53
|
clearInterval(animationInterval);
|
|
41
54
|
animationInterval = undefined;
|
|
@@ -79,18 +92,199 @@ export function createRunUiRuntime(deps) {
|
|
|
79
92
|
sendStop: async (candidate) => AsyncRuns.cancelRun(candidate.stateDir),
|
|
80
93
|
}).catch((error) => stopAfterCallbackFailure("Run retirement callback", error, ctx));
|
|
81
94
|
};
|
|
95
|
+
const journal = (ownerId) => RunDelivery.readRunDeliveryJournal(Paths.EXTENSION_RUNTIME_PATHS.tempDir, ownerId);
|
|
96
|
+
const finishPresentedBatch = (ownerId, batch) => {
|
|
97
|
+
if (batch.phase !== "presented")
|
|
98
|
+
return false;
|
|
99
|
+
for (const member of batch.members) {
|
|
100
|
+
AsyncRuns.markRunTerminalNotificationHandled(member.state_dir, member.status, member.run_instance_id);
|
|
101
|
+
}
|
|
102
|
+
return RunDelivery.finalizeRunCompletionBatch({
|
|
103
|
+
batchId: batch.batch_id,
|
|
104
|
+
ownerId,
|
|
105
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
const recoverPresentedBatch = (ownerId) => {
|
|
109
|
+
const batch = journal(ownerId).completion_batch;
|
|
110
|
+
if (batch?.phase === "presented")
|
|
111
|
+
finishPresentedBatch(ownerId, batch);
|
|
112
|
+
};
|
|
113
|
+
const finishPresentedSteer = (ownerId, steer) => {
|
|
114
|
+
if (steer.phase !== "presented")
|
|
115
|
+
return false;
|
|
116
|
+
AsyncRuns.markRunSteerPresentationHandled(steer.state_dir, steer.run_instance_id, steer.event_id, steer.steer_id);
|
|
117
|
+
return RunDelivery.finalizeRunSteer({
|
|
118
|
+
ownerId,
|
|
119
|
+
steerId: steer.steer_id,
|
|
120
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
const recoverPresentedSteers = (ownerId) => {
|
|
124
|
+
for (const steer of journal(ownerId).steers) {
|
|
125
|
+
if (steer.phase === "presented")
|
|
126
|
+
finishPresentedSteer(ownerId, steer);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const boundedSteerContent = (event) => {
|
|
130
|
+
const text = Observability.formatRunAttentionMessage(event);
|
|
131
|
+
if (Buffer.byteLength(text, "utf8") <= Limits.RUN_DELIVERY_STEER_MAX_BYTES) {
|
|
132
|
+
return text;
|
|
133
|
+
}
|
|
134
|
+
let end = Math.min(text.length, Limits.RUN_DELIVERY_STEER_MAX_BYTES - 3);
|
|
135
|
+
while (end > 0 && Buffer.byteLength(text.slice(0, end), "utf8") >
|
|
136
|
+
Limits.RUN_DELIVERY_STEER_MAX_BYTES - 3)
|
|
137
|
+
end -= 1;
|
|
138
|
+
return `${text.slice(0, end)}…`;
|
|
139
|
+
};
|
|
140
|
+
const admitSteerEvents = (ctx, ownerId, events) => {
|
|
141
|
+
for (const event of events.filter(Observability.isRunSteerAttentionEvent)) {
|
|
142
|
+
if (!event.runInstanceId) {
|
|
143
|
+
Observability.retryRunAttentionEvent(observation, event);
|
|
144
|
+
const key = `${event.stateDir}:${event.id}:missing_generation`;
|
|
145
|
+
if (!steerDiagnostics.has(key)) {
|
|
146
|
+
steerDiagnostics.add(key);
|
|
147
|
+
ctx.ui.notify(`Actor urgent steer ${event.id} remains retryable because its Run generation is unavailable.`, "warning");
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
RunDelivery.admitRunSteerEnvelope({
|
|
153
|
+
content: boundedSteerContent(event),
|
|
154
|
+
eventId: event.id,
|
|
155
|
+
kind: event.kind,
|
|
156
|
+
level: event.level,
|
|
157
|
+
occurredAt: event.ts,
|
|
158
|
+
ownerId,
|
|
159
|
+
run: event.run,
|
|
160
|
+
runInstanceId: event.runInstanceId,
|
|
161
|
+
stateDir: event.stateDir,
|
|
162
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
Observability.retryRunAttentionEvent(observation, event);
|
|
167
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
168
|
+
const key = `${event.stateDir}:${event.id}:${message}`;
|
|
169
|
+
if (!steerDiagnostics.has(key)) {
|
|
170
|
+
steerDiagnostics.add(key);
|
|
171
|
+
ctx.ui.notify(`Actor urgent steer ${event.id} remains retryable: ${message.replaceAll(/\s+/g, " ").slice(0, 240)}`, "warning");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const admitCompletionTransitions = (ownerId, transitions) => {
|
|
177
|
+
const existing = journal(ownerId).completion_batch;
|
|
178
|
+
if (existing)
|
|
179
|
+
return true;
|
|
180
|
+
const members = Observability.collectRunCompletionBatchMembers(transitions)
|
|
181
|
+
.slice(0, Limits.RUN_DELIVERY_BATCH_MAX_MEMBERS);
|
|
182
|
+
if (members.length === 0)
|
|
183
|
+
return false;
|
|
184
|
+
RunDelivery.admitRunCompletionBatch({
|
|
185
|
+
members,
|
|
186
|
+
ownerId,
|
|
187
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
188
|
+
});
|
|
189
|
+
return true;
|
|
190
|
+
};
|
|
191
|
+
const isIdle = (ctx) => typeof ctx.isIdle !== "function" || ctx.isIdle();
|
|
192
|
+
let flushCompletionBatch = (_ctx) => false;
|
|
193
|
+
const flushSteers = (ctx, ownerId) => {
|
|
194
|
+
const recovering = recoverQueuedSteers;
|
|
195
|
+
for (const steer of journal(ownerId).steers) {
|
|
196
|
+
if (steer.phase === "presented") {
|
|
197
|
+
finishPresentedSteer(ownerId, steer);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
let phase = steer.phase;
|
|
201
|
+
if (phase === "queued") {
|
|
202
|
+
if (!recovering)
|
|
203
|
+
continue;
|
|
204
|
+
const evidence = Pi.inspectRunSteerSessionEvidence(ctx, {
|
|
205
|
+
content: steer.content,
|
|
206
|
+
eventId: steer.event_id,
|
|
207
|
+
steerId: steer.steer_id,
|
|
208
|
+
});
|
|
209
|
+
if (evidence.status === "present")
|
|
210
|
+
continue;
|
|
211
|
+
if (evidence.status !== "absent") {
|
|
212
|
+
const key = `${steer.steer_id}:${evidence.status}:${evidence.reason ?? ""}`;
|
|
213
|
+
if (!steerDiagnostics.has(key)) {
|
|
214
|
+
steerDiagnostics.add(key);
|
|
215
|
+
ctx.ui.notify(`Actor urgent steer recovery is ${evidence.status}: ${evidence.reason ?? "conflicting session evidence"}. Event ${steer.event_id} remains queued.`, "warning");
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (!RunDelivery.resetRunSteerPending({
|
|
220
|
+
ownerId,
|
|
221
|
+
steerId: steer.steer_id,
|
|
222
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
223
|
+
}))
|
|
224
|
+
continue;
|
|
225
|
+
phase = "pending";
|
|
226
|
+
}
|
|
227
|
+
if (phase !== "pending")
|
|
228
|
+
continue;
|
|
229
|
+
try {
|
|
230
|
+
Pi.sendRunSteer(deps.pi, {
|
|
231
|
+
content: steer.content,
|
|
232
|
+
eventId: steer.event_id,
|
|
233
|
+
steerId: steer.steer_id,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
RunDelivery.recordRunSteerDeliveryFailure({
|
|
238
|
+
error,
|
|
239
|
+
ownerId,
|
|
240
|
+
steerId: steer.steer_id,
|
|
241
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
242
|
+
});
|
|
243
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
244
|
+
ctx.ui.notify(`Actor urgent steer delivery failed for event ${steer.event_id}: ${message.replaceAll(/\s+/g, " ").slice(0, 240)}`, "error");
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
RunDelivery.markRunSteerQueued({
|
|
248
|
+
ownerId,
|
|
249
|
+
steerId: steer.steer_id,
|
|
250
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
recoverQueuedSteers = false;
|
|
254
|
+
return journal(ownerId).steers.length > 0;
|
|
255
|
+
};
|
|
256
|
+
const scheduleCompletionFlush = () => {
|
|
257
|
+
if (!running)
|
|
258
|
+
return;
|
|
259
|
+
if (deliveryTimeout)
|
|
260
|
+
clearTimeout(deliveryTimeout);
|
|
261
|
+
deliveryTimeout = setTimeout(() => {
|
|
262
|
+
deliveryTimeout = undefined;
|
|
263
|
+
runActiveCallback("completion delivery callback", (ctx) => {
|
|
264
|
+
flushCompletionBatch(ctx);
|
|
265
|
+
});
|
|
266
|
+
}, deps.deliveryDebounceMs ?? 100);
|
|
267
|
+
deliveryTimeout.unref?.();
|
|
268
|
+
};
|
|
82
269
|
const update = (ctx, ownerId, notify = false, terminalOnly = false) => {
|
|
83
270
|
const snapshot = Observability.readRunUiSnapshot(observation, ownerId);
|
|
84
271
|
ctx.ui.setStatus("zz-pi-actors-runs", snapshot.status ? ctx.ui.theme.fg("dim", snapshot.status) : undefined);
|
|
85
272
|
if (!notify)
|
|
86
|
-
return;
|
|
273
|
+
return false;
|
|
87
274
|
const sink = Pi.createNotificationSink(deps.pi, ctx);
|
|
88
275
|
retireCandidateRuns(ctx, snapshot.summary);
|
|
89
|
-
Observability.
|
|
276
|
+
const hasCompletionCandidates = Observability.collectRunCompletionBatchMembers(snapshot.transitions).length > 0;
|
|
277
|
+
const hasCompletionBatch = Boolean(journal(ownerId).completion_batch);
|
|
278
|
+
admitSteerEvents(ctx, ownerId, snapshot.attentionEvents);
|
|
90
279
|
Observability.pruneRunUiObservationState(observation, snapshot);
|
|
91
280
|
if (!terminalOnly) {
|
|
92
|
-
Observability.deliverRunAttentionNotifications(snapshot.attentionEvents, sink);
|
|
281
|
+
Observability.deliverRunAttentionNotifications(snapshot.attentionEvents.filter((event) => !Observability.isRunSteerAttentionEvent(event)), sink);
|
|
282
|
+
}
|
|
283
|
+
const hasSteers = flushSteers(ctx, ownerId);
|
|
284
|
+
if ((hasCompletionBatch || hasCompletionCandidates) && isIdle(ctx)) {
|
|
285
|
+
scheduleCompletionFlush();
|
|
93
286
|
}
|
|
287
|
+
return hasCompletionBatch || hasCompletionCandidates || hasSteers;
|
|
94
288
|
};
|
|
95
289
|
const reportDiagnostics = (ctx) => {
|
|
96
290
|
for (const diagnostic of watcher.getDiagnostics()) {
|
|
@@ -108,8 +302,9 @@ export function createRunUiRuntime(deps) {
|
|
|
108
302
|
notifyTimeout = setTimeout(() => {
|
|
109
303
|
runActiveCallback("Run watcher callback", (ctx, ownerId) => {
|
|
110
304
|
watcher.refresh();
|
|
111
|
-
update(ctx, ownerId, true);
|
|
112
|
-
|
|
305
|
+
const completionDeferred = update(ctx, ownerId, true);
|
|
306
|
+
if (!completionDeferred)
|
|
307
|
+
deps.onRunEvent();
|
|
113
308
|
reportDiagnostics(ctx);
|
|
114
309
|
});
|
|
115
310
|
}, deps.notificationDelayMs ?? 50);
|
|
@@ -128,13 +323,7 @@ export function createRunUiRuntime(deps) {
|
|
|
128
323
|
},
|
|
129
324
|
reconcile: () => {
|
|
130
325
|
runActiveCallback("terminal reconciliation callback", (ctx, ownerId) => {
|
|
131
|
-
|
|
132
|
-
inFlight: terminalNotificationsInFlight,
|
|
133
|
-
ownerId,
|
|
134
|
-
sink: Pi.createNotificationSink(deps.pi, ctx),
|
|
135
|
-
state: observation,
|
|
136
|
-
includeAttention: true,
|
|
137
|
-
});
|
|
326
|
+
update(ctx, ownerId, true);
|
|
138
327
|
reportDiagnostics(ctx);
|
|
139
328
|
});
|
|
140
329
|
},
|
|
@@ -143,8 +332,143 @@ export function createRunUiRuntime(deps) {
|
|
|
143
332
|
watcher.refresh();
|
|
144
333
|
},
|
|
145
334
|
});
|
|
335
|
+
flushCompletionBatch = (ctx) => {
|
|
336
|
+
if (!running || activeContext !== ctx || deps.getActiveContext() !== ctx)
|
|
337
|
+
return false;
|
|
338
|
+
const ownerId = activeOwnerId;
|
|
339
|
+
if (!ownerId)
|
|
340
|
+
return false;
|
|
341
|
+
if (flushSteers(ctx, ownerId))
|
|
342
|
+
return true;
|
|
343
|
+
let batch = journal(ownerId).completion_batch;
|
|
344
|
+
if (!batch) {
|
|
345
|
+
const snapshot = Observability.readRunUiSnapshot(observation, ownerId);
|
|
346
|
+
admitCompletionTransitions(ownerId, snapshot.transitions);
|
|
347
|
+
Observability.pruneRunUiObservationState(observation, snapshot);
|
|
348
|
+
batch = journal(ownerId).completion_batch;
|
|
349
|
+
}
|
|
350
|
+
if (!batch)
|
|
351
|
+
return false;
|
|
352
|
+
if (batch.phase === "presented") {
|
|
353
|
+
finishPresentedBatch(ownerId, batch);
|
|
354
|
+
const snapshot = Observability.readRunUiSnapshot(observation, ownerId);
|
|
355
|
+
admitCompletionTransitions(ownerId, snapshot.transitions);
|
|
356
|
+
Observability.pruneRunUiObservationState(observation, snapshot);
|
|
357
|
+
batch = journal(ownerId).completion_batch;
|
|
358
|
+
if (!batch)
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
if (!isIdle(ctx))
|
|
362
|
+
return true;
|
|
363
|
+
const content = RunDelivery.formatRunCompletionBatchMessage(batch);
|
|
364
|
+
if (batch.phase === "queued") {
|
|
365
|
+
if (!recoverQueuedBatch)
|
|
366
|
+
return true;
|
|
367
|
+
recoverQueuedBatch = false;
|
|
368
|
+
const evidence = Pi.inspectRunCompletionBatchSessionEvidence(ctx, batch.batch_id, content);
|
|
369
|
+
if (evidence.status === "present")
|
|
370
|
+
return true;
|
|
371
|
+
if (evidence.status !== "absent") {
|
|
372
|
+
const diagnosticKey = `${batch.batch_id}:${evidence.status}:${evidence.reason ?? ""}`;
|
|
373
|
+
if (!deliveryRecoveryDiagnostics.has(diagnosticKey)) {
|
|
374
|
+
deliveryRecoveryDiagnostics.add(diagnosticKey);
|
|
375
|
+
ctx.ui.notify(`Actor completion recovery is ${evidence.status}: ${evidence.reason ?? "conflicting session evidence"}. Batch ${batch.batch_id} remains queued.`, "warning");
|
|
376
|
+
}
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
if (!RunDelivery.resetRunCompletionBatchPending({
|
|
380
|
+
batchId: batch.batch_id,
|
|
381
|
+
ownerId,
|
|
382
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
383
|
+
}))
|
|
384
|
+
return true;
|
|
385
|
+
batch = journal(ownerId).completion_batch;
|
|
386
|
+
}
|
|
387
|
+
if (!isIdle(ctx))
|
|
388
|
+
return true;
|
|
389
|
+
try {
|
|
390
|
+
Pi.sendRunCompletionBatch(deps.pi, batch.batch_id, content);
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
RunDelivery.recordRunCompletionBatchDeliveryFailure({
|
|
394
|
+
batchId: batch.batch_id,
|
|
395
|
+
error,
|
|
396
|
+
ownerId,
|
|
397
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
398
|
+
});
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
if (!RunDelivery.markRunCompletionBatchQueued({
|
|
402
|
+
batchId: batch.batch_id,
|
|
403
|
+
ownerId,
|
|
404
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
405
|
+
})) {
|
|
406
|
+
throw new Error("Completion batch changed before queue acknowledgment");
|
|
407
|
+
}
|
|
408
|
+
recoverQueuedBatch = false;
|
|
409
|
+
return true;
|
|
410
|
+
};
|
|
146
411
|
return {
|
|
147
412
|
close,
|
|
413
|
+
flushCompletionBatch,
|
|
414
|
+
projectContext(messages, ctx) {
|
|
415
|
+
if (!running || activeContext !== ctx || deps.getActiveContext() !== ctx) {
|
|
416
|
+
return messages;
|
|
417
|
+
}
|
|
418
|
+
const ownerId = activeOwnerId;
|
|
419
|
+
if (!ownerId)
|
|
420
|
+
return messages;
|
|
421
|
+
const steerContext = Pi.dedupeRunSteerContext(messages);
|
|
422
|
+
let contextMessages = steerContext.messages;
|
|
423
|
+
for (const steer of journal(ownerId).steers) {
|
|
424
|
+
const presented = steerContext.steers.get(steer.steer_id);
|
|
425
|
+
if (!presented ||
|
|
426
|
+
presented.eventId !== steer.event_id ||
|
|
427
|
+
presented.content !== steer.content) {
|
|
428
|
+
contextMessages = Pi.removeRunSteerFromContext(contextMessages, steer.steer_id);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (!RunDelivery.markRunSteerQueued({
|
|
432
|
+
ownerId,
|
|
433
|
+
steerId: steer.steer_id,
|
|
434
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
435
|
+
}))
|
|
436
|
+
continue;
|
|
437
|
+
if (!RunDelivery.markRunSteerPresented({
|
|
438
|
+
ownerId,
|
|
439
|
+
steerId: steer.steer_id,
|
|
440
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
441
|
+
}))
|
|
442
|
+
continue;
|
|
443
|
+
const durable = journal(ownerId).steers.find((item) => item.steer_id === steer.steer_id);
|
|
444
|
+
if (durable)
|
|
445
|
+
finishPresentedSteer(ownerId, durable);
|
|
446
|
+
}
|
|
447
|
+
const projected = Pi.dedupeRunCompletionBatchContext(contextMessages);
|
|
448
|
+
const batch = journal(ownerId).completion_batch;
|
|
449
|
+
if (!batch)
|
|
450
|
+
return projected.messages;
|
|
451
|
+
const content = projected.batches.get(batch.batch_id);
|
|
452
|
+
if (content !== RunDelivery.formatRunCompletionBatchMessage(batch)) {
|
|
453
|
+
return Pi.removeRunCompletionBatchFromContext(projected.messages, batch.batch_id);
|
|
454
|
+
}
|
|
455
|
+
if (!RunDelivery.markRunCompletionBatchQueued({
|
|
456
|
+
batchId: batch.batch_id,
|
|
457
|
+
ownerId,
|
|
458
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
459
|
+
}))
|
|
460
|
+
return projected.messages;
|
|
461
|
+
if (!RunDelivery.markRunCompletionBatchPresented({
|
|
462
|
+
batchId: batch.batch_id,
|
|
463
|
+
ownerId,
|
|
464
|
+
tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
|
|
465
|
+
}))
|
|
466
|
+
return projected.messages;
|
|
467
|
+
const presented = journal(ownerId).completion_batch;
|
|
468
|
+
if (presented)
|
|
469
|
+
finishPresentedBatch(ownerId, presented);
|
|
470
|
+
return projected.messages;
|
|
471
|
+
},
|
|
148
472
|
shutdown(eventReason, ownerId, ctx) {
|
|
149
473
|
if (!ownerId)
|
|
150
474
|
return;
|
|
@@ -162,8 +486,12 @@ export function createRunUiRuntime(deps) {
|
|
|
162
486
|
close();
|
|
163
487
|
activeContext = ctx;
|
|
164
488
|
activeOwnerId = ownerId;
|
|
489
|
+
recoverQueuedBatch = true;
|
|
490
|
+
recoverQueuedSteers = true;
|
|
165
491
|
running = true;
|
|
166
492
|
try {
|
|
493
|
+
recoverPresentedBatch(ownerId);
|
|
494
|
+
recoverPresentedSteers(ownerId);
|
|
167
495
|
Observability.primeRunAttentionState(observation, ownerId);
|
|
168
496
|
update(ctx, ownerId, true, true);
|
|
169
497
|
watcher.refresh();
|
|
@@ -11,7 +11,7 @@ export interface TraceEvent {
|
|
|
11
11
|
summary?: string;
|
|
12
12
|
data?: unknown;
|
|
13
13
|
level?: "info" | "warning" | "error";
|
|
14
|
-
attention?: "notify" | "followup";
|
|
14
|
+
attention?: "notify" | "followup" | "steer";
|
|
15
15
|
}
|
|
16
16
|
export interface AppendTraceEventInput {
|
|
17
17
|
kind: string;
|
|
@@ -84,8 +84,9 @@ function normalizeTraceEventInput(input) {
|
|
|
84
84
|
}
|
|
85
85
|
if (record.attention !== undefined &&
|
|
86
86
|
record.attention !== "notify" &&
|
|
87
|
-
record.attention !== "followup"
|
|
88
|
-
|
|
87
|
+
record.attention !== "followup" &&
|
|
88
|
+
record.attention !== "steer") {
|
|
89
|
+
throw new Error("Trace event attention must be notify, followup, or steer");
|
|
89
90
|
}
|
|
90
91
|
return {
|
|
91
92
|
kind: record.kind,
|
|
@@ -212,7 +213,8 @@ function isTraceEvent(value) {
|
|
|
212
213
|
return false;
|
|
213
214
|
return record.attention === undefined ||
|
|
214
215
|
record.attention === "notify" ||
|
|
215
|
-
record.attention === "followup"
|
|
216
|
+
record.attention === "followup" ||
|
|
217
|
+
record.attention === "steer";
|
|
216
218
|
}
|
|
217
219
|
function isCount(value) {
|
|
218
220
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
@@ -42,5 +42,21 @@ export interface SessionEvidenceReadOptions {
|
|
|
42
42
|
maxToolCalls?: number;
|
|
43
43
|
maxTurns?: number;
|
|
44
44
|
}
|
|
45
|
+
export type ActiveSessionEntryMatch = "present" | "conflict" | undefined;
|
|
46
|
+
export interface ActiveSessionEntryEvidence {
|
|
47
|
+
examinedBytes: number;
|
|
48
|
+
examinedEntries: number;
|
|
49
|
+
reason?: string;
|
|
50
|
+
status: "present" | "absent" | "conflict" | "unknown";
|
|
51
|
+
}
|
|
52
|
+
export interface ActiveSessionEntryEvidenceInput {
|
|
53
|
+
getEntry(id: string): unknown;
|
|
54
|
+
leaf: unknown;
|
|
55
|
+
match(entry: Record<string, unknown>): ActiveSessionEntryMatch;
|
|
56
|
+
maxBytes?: number;
|
|
57
|
+
maxEntries?: number;
|
|
58
|
+
}
|
|
59
|
+
/** Walk only the active parent chain under exact entry and byte ceilings. */
|
|
60
|
+
export declare function inspectBoundedActiveSessionEntries(input: ActiveSessionEntryEvidenceInput): ActiveSessionEntryEvidence;
|
|
45
61
|
export declare function redactSessionEvidenceValue(value: unknown, maxTextChars?: number, seen?: WeakSet<object>): unknown;
|
|
46
62
|
export declare function readSessionEvidence(path: string, options?: SessionEvidenceReadOptions): SessionEvidence;
|
|
@@ -6,6 +6,149 @@
|
|
|
6
6
|
import * as Limits from "./limits.js";
|
|
7
7
|
import { readJsonlFileResilient, } from "./state-readers.js";
|
|
8
8
|
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;
|
|
9
|
+
function boundedJsonStringBytes(value) {
|
|
10
|
+
let bytes = Buffer.byteLength(value, "utf8") + 2;
|
|
11
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
12
|
+
const code = value.charCodeAt(index);
|
|
13
|
+
if (code === 0x22 || code === 0x5c)
|
|
14
|
+
bytes += 1;
|
|
15
|
+
else if (code < 0x20)
|
|
16
|
+
bytes += [0x08, 0x09, 0x0a, 0x0c, 0x0d].includes(code) ? 1 : 5;
|
|
17
|
+
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
18
|
+
const next = value.charCodeAt(index + 1);
|
|
19
|
+
if (next >= 0xdc00 && next <= 0xdfff)
|
|
20
|
+
index += 1;
|
|
21
|
+
else
|
|
22
|
+
bytes += 3;
|
|
23
|
+
}
|
|
24
|
+
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
25
|
+
bytes += 3;
|
|
26
|
+
}
|
|
27
|
+
return bytes;
|
|
28
|
+
}
|
|
29
|
+
function boundedJsonBytes(value, ceiling, seen = new WeakSet()) {
|
|
30
|
+
if (value === null)
|
|
31
|
+
return 4;
|
|
32
|
+
if (typeof value === "string")
|
|
33
|
+
return boundedJsonStringBytes(value);
|
|
34
|
+
if (typeof value === "number")
|
|
35
|
+
return Number.isFinite(value) ? String(value).length : 4;
|
|
36
|
+
if (typeof value === "boolean")
|
|
37
|
+
return value ? 4 : 5;
|
|
38
|
+
if (typeof value !== "object")
|
|
39
|
+
return undefined;
|
|
40
|
+
if (seen.has(value))
|
|
41
|
+
return undefined;
|
|
42
|
+
seen.add(value);
|
|
43
|
+
let bytes = 2;
|
|
44
|
+
const values = Array.isArray(value)
|
|
45
|
+
? value.map((item) => [
|
|
46
|
+
undefined,
|
|
47
|
+
item === undefined || typeof item === "function" || typeof item === "symbol"
|
|
48
|
+
? null
|
|
49
|
+
: item,
|
|
50
|
+
])
|
|
51
|
+
: Object.entries(value).filter(([, item]) => item !== undefined && typeof item !== "function" && typeof item !== "symbol");
|
|
52
|
+
for (const [key, item] of values) {
|
|
53
|
+
if (bytes > ceiling)
|
|
54
|
+
break;
|
|
55
|
+
if (bytes > 2)
|
|
56
|
+
bytes += 1;
|
|
57
|
+
if (key !== undefined)
|
|
58
|
+
bytes += boundedJsonStringBytes(key) + 1;
|
|
59
|
+
const itemBytes = boundedJsonBytes(item, ceiling - bytes, seen);
|
|
60
|
+
if (itemBytes === undefined) {
|
|
61
|
+
seen.delete(value);
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
bytes += itemBytes;
|
|
65
|
+
}
|
|
66
|
+
seen.delete(value);
|
|
67
|
+
return bytes;
|
|
68
|
+
}
|
|
69
|
+
/** Walk only the active parent chain under exact entry and byte ceilings. */
|
|
70
|
+
export function inspectBoundedActiveSessionEntries(input) {
|
|
71
|
+
const maxBytes = input.maxBytes ?? Limits.RUN_DELIVERY_SESSION_MAX_BYTES;
|
|
72
|
+
const maxEntries = input.maxEntries ?? Limits.RUN_DELIVERY_SESSION_MAX_ENTRIES;
|
|
73
|
+
if (input.leaf === undefined || input.leaf === null) {
|
|
74
|
+
return { examinedBytes: 0, examinedEntries: 0, status: "absent" };
|
|
75
|
+
}
|
|
76
|
+
const visited = new Set();
|
|
77
|
+
let current = input.leaf;
|
|
78
|
+
let examinedBytes = 0;
|
|
79
|
+
let examinedEntries = 0;
|
|
80
|
+
while (current !== undefined) {
|
|
81
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
82
|
+
return {
|
|
83
|
+
examinedBytes,
|
|
84
|
+
examinedEntries,
|
|
85
|
+
reason: "active session entry is malformed",
|
|
86
|
+
status: "unknown",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const entry = current;
|
|
90
|
+
if (typeof entry.id !== "string" || !entry.id) {
|
|
91
|
+
return {
|
|
92
|
+
examinedBytes,
|
|
93
|
+
examinedEntries,
|
|
94
|
+
reason: "active session entry id is malformed",
|
|
95
|
+
status: "unknown",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (visited.has(entry.id)) {
|
|
99
|
+
return {
|
|
100
|
+
examinedBytes,
|
|
101
|
+
examinedEntries,
|
|
102
|
+
reason: "active session entry cycle detected",
|
|
103
|
+
status: "unknown",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
visited.add(entry.id);
|
|
107
|
+
const bytes = boundedJsonBytes(entry, maxBytes - examinedBytes);
|
|
108
|
+
if (bytes === undefined) {
|
|
109
|
+
return {
|
|
110
|
+
examinedBytes,
|
|
111
|
+
examinedEntries,
|
|
112
|
+
reason: "active session entry is not serializable",
|
|
113
|
+
status: "unknown",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (examinedEntries >= maxEntries || examinedBytes + bytes > maxBytes) {
|
|
117
|
+
return {
|
|
118
|
+
examinedBytes,
|
|
119
|
+
examinedEntries,
|
|
120
|
+
reason: "active session evidence exceeds its inspection bound",
|
|
121
|
+
status: "unknown",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
examinedEntries += 1;
|
|
125
|
+
examinedBytes += bytes;
|
|
126
|
+
const matched = input.match(entry);
|
|
127
|
+
if (matched)
|
|
128
|
+
return { examinedBytes, examinedEntries, status: matched };
|
|
129
|
+
if (entry.parentId === null) {
|
|
130
|
+
return { examinedBytes, examinedEntries, status: "absent" };
|
|
131
|
+
}
|
|
132
|
+
if (typeof entry.parentId !== "string" || !entry.parentId) {
|
|
133
|
+
return {
|
|
134
|
+
examinedBytes,
|
|
135
|
+
examinedEntries,
|
|
136
|
+
reason: "active session parent is malformed",
|
|
137
|
+
status: "unknown",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
current = input.getEntry(entry.parentId);
|
|
141
|
+
if (current === undefined) {
|
|
142
|
+
return {
|
|
143
|
+
examinedBytes,
|
|
144
|
+
examinedEntries,
|
|
145
|
+
reason: "active session parent is unavailable",
|
|
146
|
+
status: "unknown",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { examinedBytes, examinedEntries, status: "unknown" };
|
|
151
|
+
}
|
|
9
152
|
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;
|
|
10
153
|
function asRecord(value) {
|
|
11
154
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
@@ -7,7 +7,7 @@ import { mkdir, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
export const DEFAULT_TEMP_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
9
9
|
export const DEFAULT_RUN_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
10
|
-
export async function cleanupStaleTempEntries(tempDir, maxAgeMs = DEFAULT_TEMP_MAX_AGE_MS, now = Date.now(), preservedEntries = new Set(["runs"])) {
|
|
10
|
+
export async function cleanupStaleTempEntries(tempDir, maxAgeMs = DEFAULT_TEMP_MAX_AGE_MS, now = Date.now(), preservedEntries = new Set(["runs", "delivery"])) {
|
|
11
11
|
let entries;
|
|
12
12
|
let removed = 0;
|
|
13
13
|
try {
|
|
@@ -102,7 +102,9 @@ function runtimeTriage(ctx, deps) {
|
|
|
102
102
|
if (!stateDir)
|
|
103
103
|
return [];
|
|
104
104
|
return RunsTrace.readRunTraceEvents(stateDir)
|
|
105
|
-
.filter((event) => event.attention === "notify" ||
|
|
105
|
+
.filter((event) => event.attention === "notify" ||
|
|
106
|
+
event.attention === "followup" ||
|
|
107
|
+
event.attention === "steer")
|
|
106
108
|
.map((event) => ({
|
|
107
109
|
run: run.run,
|
|
108
110
|
id: event.id,
|