@pasko70/pibo 1.11.1 → 1.11.3
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/dist/apps/chat/loop-api.js +22 -6
- package/dist/apps/chat-ui/assets/{dist-BBVpFHAq.js → dist-1w_WVrcu.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DBnh8gXR.js → dist-BEd6jKzd.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BXyVMdHv.js → dist-BI1eS8pb.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CCGKu-Wj.js → dist-BLdgeEs8.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CuGiEm5l.js → dist-BQmnOdXD.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DeOnZ-pw.js → dist-BmGSbokp.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CE0MvPLM.js → dist-Bwx_CaKF.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BiGfVaXN.js → dist-CRYLB6HZ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DnQYnLQS.js → dist-CoUOMSbW.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-VT4x40uL.js → dist-Dehi8o5p.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-xtnVygdr.js → dist-o1kTkdhi.js} +1 -1
- package/dist/apps/chat-ui/assets/{index-DNeE4HrG.css → index-CYLZe0Y0.css} +1 -1
- package/dist/apps/chat-ui/assets/{index-vcg8JNj9.js → index-DT80TM0S.js} +12 -12
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/{pibo-vscode-ext-1.11.1.vsix → pibo-vscode-ext-1.11.3.vsix} +0 -0
- package/dist/core/gateway-resource-guard.js +51 -5
- package/dist/core/routed-session.js +143 -24
- package/dist/core/runtime-telemetry.js +90 -0
- package/dist/core/runtime.js +1 -0
- package/dist/core/session-router.js +285 -62
- package/dist/debug/index.js +3 -1
- package/dist/gateway/server.js +2 -0
- package/dist/gateway/web.js +1 -0
- package/dist/loops/accounting.js +8 -1
- package/dist/loops/cli.js +1 -1
- package/dist/loops/service.js +167 -27
- package/dist/loops/store.js +229 -17
- package/dist/loops/tools.js +30 -9
- package/dist/reliability/store.js +19 -5
- package/dist/runs/registry.js +29 -7
- package/package.json +1 -1
|
@@ -16,13 +16,14 @@ import { loadPiboUserSettings } from "./user-settings.js";
|
|
|
16
16
|
import { resolvePiboSessionActiveModel } from "./session-model.js";
|
|
17
17
|
import { isPiboThinkingLevel } from "./thinking.js";
|
|
18
18
|
import { RuntimeSessionRegistry } from "../tools/runtime/registry.js";
|
|
19
|
-
import {
|
|
19
|
+
import { GatewayWorkAdmissionController } from "./gateway-resource-guard.js";
|
|
20
20
|
import { withWorkflowSessionKind } from "../sessions/workflow-session-kind.js";
|
|
21
21
|
import { PiboRuntimeTelemetryRecorder } from "./runtime-telemetry.js";
|
|
22
22
|
import { createPiboProviderTelemetryExtension } from "./provider-telemetry.js";
|
|
23
23
|
import { AsyncTelemetryWriter } from "../data/telemetry-writer.js";
|
|
24
24
|
const DEFAULT_SUBAGENT_REPLY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
25
25
|
const DEFAULT_ROUTED_SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
26
|
+
const DEFAULT_ROUTED_SESSION_DISPOSE_TIMEOUT_MS = 30 * 1000;
|
|
26
27
|
export const LOOP_RUNTIME_RETRY_DEFAULTS = {
|
|
27
28
|
enabled: true,
|
|
28
29
|
maxRetries: 7,
|
|
@@ -115,7 +116,7 @@ function formatRunReminderMessage(notification) {
|
|
|
115
116
|
].join("\n");
|
|
116
117
|
}
|
|
117
118
|
function isRunReminderServiceMessage(event) {
|
|
118
|
-
return event.source === "service" && event.
|
|
119
|
+
return event.source === "service" && event.capabilityScope === "run-reminder";
|
|
119
120
|
}
|
|
120
121
|
function isTerminalRunStatus(status) {
|
|
121
122
|
return status === "completed" || status === "failed" || status === "timed_out" || status === "cancelled";
|
|
@@ -130,6 +131,16 @@ function piboRoomIdFromMetadata(metadata) {
|
|
|
130
131
|
const value = metadata?.chatRoomId;
|
|
131
132
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
132
133
|
}
|
|
134
|
+
class PiboSessionDisposalTimeoutError extends Error {
|
|
135
|
+
piboSessionId;
|
|
136
|
+
timeoutMs;
|
|
137
|
+
constructor(piboSessionId, timeoutMs) {
|
|
138
|
+
super(`Timed out disposing Pibo session "${piboSessionId}" after ${timeoutMs}ms`);
|
|
139
|
+
this.piboSessionId = piboSessionId;
|
|
140
|
+
this.timeoutMs = timeoutMs;
|
|
141
|
+
this.name = "PiboSessionDisposalTimeoutError";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
133
144
|
function telemetryStoreFromSessionStore(store) {
|
|
134
145
|
return store.getTelemetryStore?.();
|
|
135
146
|
}
|
|
@@ -143,11 +154,16 @@ export class PiboSessionRouter {
|
|
|
143
154
|
pendingSessions = new Map();
|
|
144
155
|
listeners = new Set();
|
|
145
156
|
runRegistry;
|
|
157
|
+
gatewayWorkAdmission = new GatewayWorkAdmissionController();
|
|
146
158
|
signalRegistry;
|
|
147
159
|
runtimeRegistry;
|
|
148
160
|
scheduledRunReminders = new Map();
|
|
161
|
+
runReminderGenerations = new Map();
|
|
162
|
+
quiescingSessions = new Set();
|
|
163
|
+
disposingSessions = new Map();
|
|
149
164
|
idleSessionTimers = new Map();
|
|
150
165
|
routedSessionIdleTimeoutMs;
|
|
166
|
+
routedSessionDisposeTimeoutMs;
|
|
151
167
|
baseProfile;
|
|
152
168
|
pluginRegistry;
|
|
153
169
|
sessionStore;
|
|
@@ -175,6 +191,10 @@ export class PiboSessionRouter {
|
|
|
175
191
|
: typeof idleTimeoutMs === "number" && Number.isFinite(idleTimeoutMs) && idleTimeoutMs > 0
|
|
176
192
|
? idleTimeoutMs
|
|
177
193
|
: DEFAULT_ROUTED_SESSION_IDLE_TIMEOUT_MS;
|
|
194
|
+
const disposeTimeoutMs = options.routedSessionDisposeTimeoutMs;
|
|
195
|
+
this.routedSessionDisposeTimeoutMs = typeof disposeTimeoutMs === "number" && Number.isFinite(disposeTimeoutMs) && disposeTimeoutMs > 0
|
|
196
|
+
? disposeTimeoutMs
|
|
197
|
+
: DEFAULT_ROUTED_SESSION_DISPOSE_TIMEOUT_MS;
|
|
178
198
|
const defaultProfileName = selectDefaultPiboProfileName(this.pluginRegistry);
|
|
179
199
|
this.baseProfile = options.profile ?? createPiboProfileFromRegistryOrDefault(this.pluginRegistry, defaultProfileName);
|
|
180
200
|
this.reliabilityStore = options.reliabilityStore ?? (options.persistSession === false ? undefined : createDefaultPiboReliabilityStore());
|
|
@@ -195,6 +215,16 @@ export class PiboSessionRouter {
|
|
|
195
215
|
async emit(event) {
|
|
196
216
|
if (this.closing)
|
|
197
217
|
throw new Error("Pibo session router is disposed.");
|
|
218
|
+
const teardownAction = event.type === "execution" && (event.action === "dispose" || event.action === "kill" || event.action === "kill_all");
|
|
219
|
+
const teardownIds = teardownAction
|
|
220
|
+
? [event.piboSessionId, ...this.descendantSessionIds(event.piboSessionId)]
|
|
221
|
+
: [];
|
|
222
|
+
if (event.type === "execution" && event.action === "abort") {
|
|
223
|
+
this.invalidateRunReminders([event.piboSessionId]);
|
|
224
|
+
}
|
|
225
|
+
else if (teardownAction) {
|
|
226
|
+
this.invalidateRunReminders(teardownIds);
|
|
227
|
+
}
|
|
198
228
|
if (event.type === "message" && event.id) {
|
|
199
229
|
const stored = this.sessionStore.get(event.piboSessionId);
|
|
200
230
|
if (stored)
|
|
@@ -220,6 +250,9 @@ export class PiboSessionRouter {
|
|
|
220
250
|
throw error;
|
|
221
251
|
}
|
|
222
252
|
this.clearIdleSessionTimer(event.piboSessionId);
|
|
253
|
+
let teardownCompleted = false;
|
|
254
|
+
if (teardownAction)
|
|
255
|
+
this.beginSessionQuiescence(teardownIds);
|
|
223
256
|
try {
|
|
224
257
|
if (event.type === "message") {
|
|
225
258
|
return event.delivery === "steer"
|
|
@@ -232,12 +265,23 @@ export class PiboSessionRouter {
|
|
|
232
265
|
else if (event.action === "dispose" || event.action === "kill" || event.action === "kill_all") {
|
|
233
266
|
this.signalRegistry.project({ type: "session_disposed", piboSessionId: event.piboSessionId, reason: `${event.action} action` });
|
|
234
267
|
}
|
|
235
|
-
const output = await session.executeAction(event);
|
|
236
268
|
if (event.action === "dispose") {
|
|
269
|
+
const output = {
|
|
270
|
+
type: "execution_result",
|
|
271
|
+
piboSessionId: event.piboSessionId,
|
|
272
|
+
eventId: event.id,
|
|
273
|
+
action: event.action,
|
|
274
|
+
result: { disposed: true },
|
|
275
|
+
};
|
|
276
|
+
this.emitOutput(output);
|
|
237
277
|
await this.disposeSessionSubtree(event.piboSessionId, "dispose action", { cancelRuns: true });
|
|
278
|
+
teardownCompleted = true;
|
|
279
|
+
return output;
|
|
238
280
|
}
|
|
239
|
-
|
|
281
|
+
const output = await session.executeAction(event);
|
|
282
|
+
if (event.action === "kill" || event.action === "kill_all") {
|
|
240
283
|
await this.disposeSessionSubtree(event.piboSessionId, `${event.action} action`, { cancelRuns: event.action === "kill_all" });
|
|
284
|
+
teardownCompleted = true;
|
|
241
285
|
}
|
|
242
286
|
else if (shouldResetSessionAfterAction(event.action)) {
|
|
243
287
|
await this.resetCachedSession(event.piboSessionId, "provider auth changed");
|
|
@@ -245,6 +289,9 @@ export class PiboSessionRouter {
|
|
|
245
289
|
return output;
|
|
246
290
|
}
|
|
247
291
|
catch (error) {
|
|
292
|
+
if (teardownAction && !teardownCompleted) {
|
|
293
|
+
await this.disposeSessionSubtree(event.piboSessionId, `${event.action} action failed`, { cancelRuns: event.action === "dispose" || event.action === "kill_all" }).catch(() => { });
|
|
294
|
+
}
|
|
248
295
|
if (event.type === "message" && event.id) {
|
|
249
296
|
this.signalRegistry.project({
|
|
250
297
|
type: "message_rejected",
|
|
@@ -266,46 +313,118 @@ export class PiboSessionRouter {
|
|
|
266
313
|
}
|
|
267
314
|
}
|
|
268
315
|
async killSession(piboSessionId, options) {
|
|
316
|
+
const rootSession = this.sessions.get(piboSessionId);
|
|
317
|
+
if (!rootSession)
|
|
318
|
+
return { killed: [], cancelledRuns: [] };
|
|
319
|
+
const ids = [piboSessionId, ...this.descendantSessionIds(piboSessionId)];
|
|
320
|
+
this.beginSessionQuiescence(ids);
|
|
269
321
|
const killed = [];
|
|
270
322
|
const cancelledRuns = [];
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
this.
|
|
274
|
-
|
|
323
|
+
const failures = [];
|
|
324
|
+
for (const id of ids) {
|
|
325
|
+
const session = this.sessions.get(id);
|
|
326
|
+
if (session) {
|
|
327
|
+
this.signalRegistry.project({ type: "session_disposed", piboSessionId: id, reason: "kill" });
|
|
328
|
+
try {
|
|
329
|
+
killed.push(await session.kill());
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
failures.push(error);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
275
335
|
if (options?.includeRuns) {
|
|
276
|
-
const runs = this.runRegistry.cancelControllerRuns(
|
|
277
|
-
cancelledRuns.push(...runs.map((
|
|
336
|
+
const runs = this.runRegistry.cancelControllerRuns(id);
|
|
337
|
+
cancelledRuns.push(...runs.map((run) => run.runId));
|
|
278
338
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
cancelledRuns.push(...children.cancelledRuns);
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
282
341
|
await this.disposeSessionSubtree(piboSessionId, "kill", { cancelRuns: false });
|
|
283
342
|
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
failures.push(error);
|
|
345
|
+
}
|
|
346
|
+
if (failures.length > 0)
|
|
347
|
+
throw new AggregateError(failures, `Failed to kill Pibo session subtree "${piboSessionId}"`);
|
|
284
348
|
return { killed, cancelledRuns };
|
|
285
349
|
}
|
|
350
|
+
async disposeRoutedSession(piboSessionId, session, reason) {
|
|
351
|
+
const disposal = Promise.resolve().then(() => session.dispose());
|
|
352
|
+
let timeout;
|
|
353
|
+
const timedOut = new Promise((_resolve, reject) => {
|
|
354
|
+
timeout = setTimeout(() => reject(new PiboSessionDisposalTimeoutError(piboSessionId, this.routedSessionDisposeTimeoutMs)), this.routedSessionDisposeTimeoutMs);
|
|
355
|
+
timeout.unref?.();
|
|
356
|
+
});
|
|
357
|
+
try {
|
|
358
|
+
await Promise.race([disposal, timedOut]);
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
if (error instanceof PiboSessionDisposalTimeoutError) {
|
|
362
|
+
session.forceDispose(`${reason}; bounded disposal timeout`);
|
|
363
|
+
void disposal.catch(() => { });
|
|
364
|
+
}
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
if (timeout)
|
|
369
|
+
clearTimeout(timeout);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
286
372
|
async disposeSessionSubtree(piboSessionId, reason, options) {
|
|
287
373
|
const ids = [piboSessionId, ...this.descendantSessionIds(piboSessionId)];
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
374
|
+
const existingDisposals = [...new Set(ids.map((id) => this.disposingSessions.get(id)).filter((value) => Boolean(value)))];
|
|
375
|
+
if (existingDisposals.length > 0)
|
|
376
|
+
await Promise.all(existingDisposals);
|
|
377
|
+
this.beginSessionQuiescence(ids);
|
|
378
|
+
if (options.cancelRuns) {
|
|
379
|
+
for (const id of ids)
|
|
291
380
|
this.runRegistry.cancelControllerRuns(id);
|
|
292
|
-
this.clearIdleSessionTimer(id);
|
|
293
|
-
this.scheduledRunReminders.delete(id);
|
|
294
|
-
const cached = this.sessions.get(id);
|
|
295
|
-
if (cached)
|
|
296
|
-
sessions.push(cached);
|
|
297
|
-
this.sessions.delete(id);
|
|
298
381
|
}
|
|
382
|
+
let releaseStart;
|
|
383
|
+
const startGate = new Promise((resolve) => {
|
|
384
|
+
releaseStart = resolve;
|
|
385
|
+
});
|
|
386
|
+
const operation = (async () => {
|
|
387
|
+
await startGate;
|
|
388
|
+
const pending = ids.map((id) => this.pendingSessions.get(id)).filter((value) => Boolean(value));
|
|
389
|
+
if (pending.length > 0)
|
|
390
|
+
await Promise.allSettled(pending);
|
|
391
|
+
const sessions = ids.flatMap((id) => {
|
|
392
|
+
const session = this.sessions.get(id);
|
|
393
|
+
return session ? [{ id, session }] : [];
|
|
394
|
+
});
|
|
395
|
+
const failures = [];
|
|
396
|
+
const closeResults = await Promise.allSettled(ids.map((id) => this.runtimeRegistry.closeControllerSessions(id, { force: true })));
|
|
397
|
+
for (const result of closeResults) {
|
|
398
|
+
if (result.status === "rejected")
|
|
399
|
+
failures.push(result.reason);
|
|
400
|
+
}
|
|
401
|
+
const disposeResults = await Promise.allSettled(sessions.map(({ id, session }) => this.disposeRoutedSession(id, session, reason)));
|
|
402
|
+
for (const result of disposeResults) {
|
|
403
|
+
if (result.status === "rejected")
|
|
404
|
+
failures.push(result.reason);
|
|
405
|
+
}
|
|
406
|
+
for (const { id, session } of sessions) {
|
|
407
|
+
if (this.sessions.get(id) === session)
|
|
408
|
+
this.sessions.delete(id);
|
|
409
|
+
}
|
|
410
|
+
if (failures.length > 0)
|
|
411
|
+
throw new AggregateError(failures, `Failed to dispose Pibo session subtree "${piboSessionId}"`);
|
|
412
|
+
})();
|
|
413
|
+
for (const id of ids)
|
|
414
|
+
this.disposingSessions.set(id, operation);
|
|
415
|
+
releaseStart?.();
|
|
299
416
|
try {
|
|
300
|
-
await
|
|
301
|
-
await Promise.all(sessions.map((session) => session.dispose()));
|
|
417
|
+
await operation;
|
|
302
418
|
}
|
|
303
419
|
finally {
|
|
420
|
+
for (const id of ids) {
|
|
421
|
+
if (this.disposingSessions.get(id) === operation)
|
|
422
|
+
this.disposingSessions.delete(id);
|
|
423
|
+
this.quiescingSessions.delete(id);
|
|
424
|
+
this.signalRegistry.project({ type: "session_disposed", piboSessionId: id, reason });
|
|
425
|
+
}
|
|
304
426
|
await this.telemetryWriter?.flush();
|
|
305
427
|
}
|
|
306
|
-
for (const id of ids) {
|
|
307
|
-
this.signalRegistry.project({ type: "session_disposed", piboSessionId: id, reason });
|
|
308
|
-
}
|
|
309
428
|
}
|
|
310
429
|
descendantSessionIds(parentId) {
|
|
311
430
|
const output = [];
|
|
@@ -456,18 +575,28 @@ export class PiboSessionRouter {
|
|
|
456
575
|
}
|
|
457
576
|
async disposeAllUnsafe() {
|
|
458
577
|
try {
|
|
578
|
+
const initialIds = [...new Set([...this.sessions.keys(), ...this.pendingSessions.keys()])];
|
|
579
|
+
this.beginSessionQuiescence(initialIds);
|
|
459
580
|
await Promise.allSettled([...this.pendingSessions.values()]);
|
|
460
|
-
const sessions = [...this.sessions.
|
|
461
|
-
this.sessions.clear();
|
|
581
|
+
const sessions = [...this.sessions.entries()];
|
|
462
582
|
for (const timer of this.idleSessionTimers.values())
|
|
463
583
|
clearTimeout(timer);
|
|
464
584
|
this.idleSessionTimers.clear();
|
|
465
585
|
this.runRegistry.cancelAll("Pibo session router was disposed.");
|
|
466
|
-
for (const session of sessions)
|
|
467
|
-
this.signalRegistry.project({ type: "session_disposed", piboSessionId: session.getStatus().piboSessionId, reason: "router disposed" });
|
|
468
586
|
this.scheduledRunReminders.clear();
|
|
469
|
-
await this.runtimeRegistry.closeAll({ force: true });
|
|
470
|
-
await Promise.
|
|
587
|
+
const closeResult = await Promise.allSettled([this.runtimeRegistry.closeAll({ force: true })]);
|
|
588
|
+
const disposeResults = await Promise.allSettled(sessions.map(([id, session]) => this.disposeRoutedSession(id, session, "router disposed")));
|
|
589
|
+
for (const [id, session] of sessions) {
|
|
590
|
+
if (this.sessions.get(id) === session)
|
|
591
|
+
this.sessions.delete(id);
|
|
592
|
+
this.signalRegistry.project({ type: "session_disposed", piboSessionId: id, reason: "router disposed" });
|
|
593
|
+
}
|
|
594
|
+
const failures = [
|
|
595
|
+
...closeResult.filter((result) => result.status === "rejected").map((result) => result.reason),
|
|
596
|
+
...disposeResults.filter((result) => result.status === "rejected").map((result) => result.reason),
|
|
597
|
+
];
|
|
598
|
+
if (failures.length > 0)
|
|
599
|
+
throw new AggregateError(failures, "Failed to dispose all Pibo sessions");
|
|
471
600
|
}
|
|
472
601
|
finally {
|
|
473
602
|
await this.telemetryWriter?.dispose();
|
|
@@ -518,6 +647,14 @@ export class PiboSessionRouter {
|
|
|
518
647
|
async getOrCreateSession(piboSessionId) {
|
|
519
648
|
if (this.closing)
|
|
520
649
|
throw new Error("Pibo session router is disposed.");
|
|
650
|
+
if (this.quiescingSessions.has(piboSessionId)) {
|
|
651
|
+
throw new Error(`Pibo session "${piboSessionId}" is quiescing.`);
|
|
652
|
+
}
|
|
653
|
+
const disposing = this.disposingSessions.get(piboSessionId);
|
|
654
|
+
if (disposing) {
|
|
655
|
+
await disposing;
|
|
656
|
+
return await this.getOrCreateSession(piboSessionId);
|
|
657
|
+
}
|
|
521
658
|
const existing = this.sessions.get(piboSessionId);
|
|
522
659
|
if (existing) {
|
|
523
660
|
this.clearIdleSessionTimer(piboSessionId);
|
|
@@ -537,6 +674,7 @@ export class PiboSessionRouter {
|
|
|
537
674
|
}
|
|
538
675
|
async createRoutedSession(piboSessionId) {
|
|
539
676
|
const piboSession = this.resolvePiboSession(piboSessionId);
|
|
677
|
+
let session;
|
|
540
678
|
this.signalRegistry.project({ type: "session_created", session: piboSession });
|
|
541
679
|
const profile = createPiboProfileFromRegistryOrDefault(this.pluginRegistry, piboSession.profile);
|
|
542
680
|
const parentPiSessionId = piboSession.parentId
|
|
@@ -568,10 +706,11 @@ export class PiboSessionRouter {
|
|
|
568
706
|
piboSessionId: piboSession.id,
|
|
569
707
|
piboRoomId: piboRoomIdFromMetadata(piboSession.metadata),
|
|
570
708
|
timezone: userSettings.timezone,
|
|
709
|
+
getActiveMessage: () => session?.getActiveMessage(),
|
|
571
710
|
},
|
|
572
711
|
});
|
|
573
712
|
const initialFastMode = resolvePiboSessionInitialFastMode(piboSession) ?? selectRequestedFastMode(profileForSession(profile, piboSession.piSessionId, parentPiSessionId), modelDefaults) ?? false;
|
|
574
|
-
|
|
713
|
+
session = new RoutedSession(piboSession.id, runtime, this.emitOutput, this.pluginRegistry, this.options.forwardPiEvents ?? false, this.telemetryRecorder
|
|
575
714
|
? (id, event, context) => this.telemetryRecorder?.recordPiEvent(id, event, { session: this.sessionStore.get(id), status: context.status, activeEventId: context.activeEventId })
|
|
576
715
|
: undefined, initialFastMode, (result, event) => this.handleSessionOperation(result, event), (id, opts) => this.killChildSessions(id, opts), (state) => {
|
|
577
716
|
this.signalRegistry.project({ type: "session_processing_changed", piboSessionId: piboSession.id, processing: state.processing, queuedMessages: state.queuedMessages });
|
|
@@ -582,7 +721,7 @@ export class PiboSessionRouter {
|
|
|
582
721
|
}, (messages, reason) => this.telemetryRecorder?.recordMessagesInterrupted(messages, {
|
|
583
722
|
session: this.sessionStore.get(piboSession.id),
|
|
584
723
|
status: this.sessions.get(piboSession.id)?.getStatus(),
|
|
585
|
-
}, reason));
|
|
724
|
+
}, reason), this.options.messagePreflight);
|
|
586
725
|
this.sessions.set(piboSession.id, session);
|
|
587
726
|
return session;
|
|
588
727
|
}
|
|
@@ -640,14 +779,42 @@ export class PiboSessionRouter {
|
|
|
640
779
|
});
|
|
641
780
|
}
|
|
642
781
|
async resetCachedSession(piboSessionId, reason) {
|
|
643
|
-
const
|
|
782
|
+
const existingDisposal = this.disposingSessions.get(piboSessionId);
|
|
783
|
+
if (existingDisposal)
|
|
784
|
+
await existingDisposal;
|
|
644
785
|
this.clearIdleSessionTimer(piboSessionId);
|
|
645
|
-
|
|
786
|
+
let releaseStart;
|
|
787
|
+
const startGate = new Promise((resolve) => {
|
|
788
|
+
releaseStart = resolve;
|
|
789
|
+
});
|
|
790
|
+
const operation = (async () => {
|
|
791
|
+
await startGate;
|
|
792
|
+
const pending = this.pendingSessions.get(piboSessionId);
|
|
793
|
+
if (pending)
|
|
794
|
+
await Promise.allSettled([pending]);
|
|
795
|
+
const cached = this.sessions.get(piboSessionId);
|
|
796
|
+
const failures = [];
|
|
797
|
+
const closeResult = await Promise.allSettled([this.runtimeRegistry.closeControllerSessions(piboSessionId, { force: true })]);
|
|
798
|
+
if (closeResult[0]?.status === "rejected")
|
|
799
|
+
failures.push(closeResult[0].reason);
|
|
800
|
+
if (cached) {
|
|
801
|
+
const disposeResult = await Promise.allSettled([this.disposeRoutedSession(piboSessionId, cached, reason ?? "session reset")]);
|
|
802
|
+
if (disposeResult[0]?.status === "rejected")
|
|
803
|
+
failures.push(disposeResult[0].reason);
|
|
804
|
+
if (this.sessions.get(piboSessionId) === cached)
|
|
805
|
+
this.sessions.delete(piboSessionId);
|
|
806
|
+
}
|
|
807
|
+
if (failures.length > 0)
|
|
808
|
+
throw new AggregateError(failures, `Failed to reset Pibo session "${piboSessionId}"`);
|
|
809
|
+
})();
|
|
810
|
+
this.disposingSessions.set(piboSessionId, operation);
|
|
811
|
+
releaseStart?.();
|
|
646
812
|
try {
|
|
647
|
-
await
|
|
648
|
-
await cached?.dispose();
|
|
813
|
+
await operation;
|
|
649
814
|
}
|
|
650
815
|
finally {
|
|
816
|
+
if (this.disposingSessions.get(piboSessionId) === operation)
|
|
817
|
+
this.disposingSessions.delete(piboSessionId);
|
|
651
818
|
await this.telemetryWriter?.flush();
|
|
652
819
|
}
|
|
653
820
|
if (reason)
|
|
@@ -697,23 +864,31 @@ export class PiboSessionRouter {
|
|
|
697
864
|
createRunToolController(parentPiboSessionId) {
|
|
698
865
|
return {
|
|
699
866
|
startToolRun: ({ toolName, params, completionPolicy, retryable, maxAttempts, timeoutMs, serviceWarning, execute }) => {
|
|
700
|
-
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
867
|
+
const admission = this.gatewayWorkAdmission.reserve(`yielded run ${toolName}`);
|
|
868
|
+
const reminderGeneration = this.runReminderGeneration(parentPiboSessionId);
|
|
869
|
+
let run;
|
|
870
|
+
try {
|
|
871
|
+
run = this.runRegistry.startToolRun({
|
|
872
|
+
controllerPiboSessionId: parentPiboSessionId,
|
|
873
|
+
toolName,
|
|
874
|
+
params,
|
|
875
|
+
completionPolicy,
|
|
876
|
+
retryable,
|
|
877
|
+
maxAttempts,
|
|
878
|
+
timeoutMs,
|
|
879
|
+
serviceWarning,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
catch (error) {
|
|
883
|
+
admission.release();
|
|
884
|
+
throw error;
|
|
885
|
+
}
|
|
711
886
|
void (async () => {
|
|
712
887
|
try {
|
|
713
888
|
const result = await execute();
|
|
714
889
|
const completed = this.runRegistry.complete(run.runId, result);
|
|
715
890
|
if (completed)
|
|
716
|
-
this.
|
|
891
|
+
this.handleTerminalRunReminder(parentPiboSessionId, completed.runId, reminderGeneration);
|
|
717
892
|
}
|
|
718
893
|
catch (error) {
|
|
719
894
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -721,7 +896,10 @@ export class PiboSessionRouter {
|
|
|
721
896
|
? this.runRegistry.timeOut(run.runId, message, error.timeoutPhase)
|
|
722
897
|
: this.runRegistry.fail(run.runId, message);
|
|
723
898
|
if (terminalRun)
|
|
724
|
-
this.
|
|
899
|
+
this.handleTerminalRunReminder(parentPiboSessionId, terminalRun.runId, reminderGeneration);
|
|
900
|
+
}
|
|
901
|
+
finally {
|
|
902
|
+
admission.release();
|
|
725
903
|
}
|
|
726
904
|
})();
|
|
727
905
|
return run;
|
|
@@ -731,9 +909,8 @@ export class PiboSessionRouter {
|
|
|
731
909
|
waitForRun: (runId, timeoutMs) => this.runRegistry.wait(parentPiboSessionId, runId, timeoutMs),
|
|
732
910
|
readRun: (runId) => {
|
|
733
911
|
const run = this.runRegistry.read(parentPiboSessionId, runId);
|
|
734
|
-
if (run.consumed && isTerminalRunStatus(run.status))
|
|
912
|
+
if (run.consumed && isTerminalRunStatus(run.status))
|
|
735
913
|
this.refreshQueuedRunReminders(parentPiboSessionId);
|
|
736
|
-
}
|
|
737
914
|
return run;
|
|
738
915
|
},
|
|
739
916
|
cancelRun: async (runId) => {
|
|
@@ -846,17 +1023,54 @@ export class PiboSessionRouter {
|
|
|
846
1023
|
}
|
|
847
1024
|
this.signalRegistry.project({ type: "run_changed", run: event.run, previousStatus: "previousStatus" in event ? event.previousStatus : undefined, reason: "reason" in event ? event.reason : event.type });
|
|
848
1025
|
}
|
|
849
|
-
|
|
1026
|
+
runReminderGeneration(piboSessionId) {
|
|
1027
|
+
return this.runReminderGenerations.get(piboSessionId) ?? 0;
|
|
1028
|
+
}
|
|
1029
|
+
invalidateRunReminders(piboSessionIds) {
|
|
1030
|
+
for (const piboSessionId of piboSessionIds) {
|
|
1031
|
+
this.runReminderGenerations.set(piboSessionId, this.runReminderGeneration(piboSessionId) + 1);
|
|
1032
|
+
this.scheduledRunReminders.delete(piboSessionId);
|
|
1033
|
+
try {
|
|
1034
|
+
this.sessions.get(piboSessionId)?.removeQueuedMessages(isRunReminderServiceMessage);
|
|
1035
|
+
}
|
|
1036
|
+
catch {
|
|
1037
|
+
// A concurrently disposed RoutedSession is already quiescent.
|
|
1038
|
+
}
|
|
1039
|
+
this.runRegistry.suppressControllerNotifications(piboSessionId);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
beginSessionQuiescence(piboSessionIds) {
|
|
1043
|
+
this.invalidateRunReminders(piboSessionIds);
|
|
1044
|
+
for (const piboSessionId of piboSessionIds) {
|
|
1045
|
+
this.quiescingSessions.add(piboSessionId);
|
|
1046
|
+
this.clearIdleSessionTimer(piboSessionId);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
handleTerminalRunReminder(piboSessionId, runId, generation) {
|
|
1050
|
+
if (generation !== this.runReminderGeneration(piboSessionId) || this.quiescingSessions.has(piboSessionId) || this.closing) {
|
|
1051
|
+
this.runRegistry.suppressNotification(piboSessionId, runId);
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
this.scheduleRunReminder(piboSessionId, false, generation);
|
|
1055
|
+
}
|
|
1056
|
+
scheduleRunReminder(piboSessionId, includeAlreadyNotified, expectedGeneration = this.runReminderGeneration(piboSessionId)) {
|
|
1057
|
+
if (this.closing || this.quiescingSessions.has(piboSessionId))
|
|
1058
|
+
return;
|
|
1059
|
+
if (expectedGeneration !== this.runReminderGeneration(piboSessionId))
|
|
1060
|
+
return;
|
|
850
1061
|
if (!this.runRegistry.hasPendingNotification(piboSessionId, { includeAlreadyNotified }))
|
|
851
1062
|
return;
|
|
852
1063
|
const previous = this.scheduledRunReminders.get(piboSessionId);
|
|
853
|
-
if (previous
|
|
854
|
-
this.scheduledRunReminders.set(piboSessionId,
|
|
1064
|
+
if (previous?.generation === expectedGeneration) {
|
|
1065
|
+
this.scheduledRunReminders.set(piboSessionId, {
|
|
1066
|
+
generation: expectedGeneration,
|
|
1067
|
+
includeAlreadyNotified: previous.includeAlreadyNotified || includeAlreadyNotified,
|
|
1068
|
+
});
|
|
855
1069
|
return;
|
|
856
1070
|
}
|
|
857
|
-
this.scheduledRunReminders.set(piboSessionId, includeAlreadyNotified);
|
|
1071
|
+
this.scheduledRunReminders.set(piboSessionId, { generation: expectedGeneration, includeAlreadyNotified });
|
|
858
1072
|
queueMicrotask(() => {
|
|
859
|
-
void this.deliverRunReminder(piboSessionId);
|
|
1073
|
+
void this.deliverRunReminder(piboSessionId, expectedGeneration);
|
|
860
1074
|
});
|
|
861
1075
|
}
|
|
862
1076
|
refreshQueuedRunReminders(piboSessionId) {
|
|
@@ -864,23 +1078,32 @@ export class PiboSessionRouter {
|
|
|
864
1078
|
if (removed > 0)
|
|
865
1079
|
this.scheduleRunReminder(piboSessionId, true);
|
|
866
1080
|
}
|
|
867
|
-
async deliverRunReminder(piboSessionId) {
|
|
868
|
-
const
|
|
1081
|
+
async deliverRunReminder(piboSessionId, expectedGeneration) {
|
|
1082
|
+
const scheduled = this.scheduledRunReminders.get(piboSessionId);
|
|
1083
|
+
if (!scheduled || scheduled.generation !== expectedGeneration)
|
|
1084
|
+
return;
|
|
869
1085
|
this.scheduledRunReminders.delete(piboSessionId);
|
|
870
|
-
|
|
1086
|
+
if (this.closing || this.quiescingSessions.has(piboSessionId) || expectedGeneration !== this.runReminderGeneration(piboSessionId))
|
|
1087
|
+
return;
|
|
1088
|
+
const notification = this.runRegistry.createNotification(piboSessionId, { includeAlreadyNotified: scheduled.includeAlreadyNotified });
|
|
871
1089
|
if (!notification)
|
|
872
1090
|
return;
|
|
873
1091
|
try {
|
|
874
1092
|
const session = await this.getOrCreateSession(piboSessionId);
|
|
1093
|
+
if (this.closing || this.quiescingSessions.has(piboSessionId) || expectedGeneration !== this.runReminderGeneration(piboSessionId))
|
|
1094
|
+
return;
|
|
875
1095
|
session.enqueueMessage({
|
|
876
1096
|
type: "message",
|
|
877
1097
|
piboSessionId,
|
|
878
1098
|
text: formatRunReminderMessage(notification),
|
|
879
1099
|
source: "service",
|
|
1100
|
+
capabilityScope: "run-reminder",
|
|
880
1101
|
id: randomUUID(),
|
|
881
1102
|
});
|
|
882
1103
|
}
|
|
883
1104
|
catch (error) {
|
|
1105
|
+
if (this.closing || this.quiescingSessions.has(piboSessionId) || expectedGeneration !== this.runReminderGeneration(piboSessionId))
|
|
1106
|
+
return;
|
|
884
1107
|
const message = error instanceof Error ? error.message : String(error);
|
|
885
1108
|
this.emitOutput({
|
|
886
1109
|
type: "session_error",
|
package/dist/debug/index.js
CHANGED
|
@@ -1058,11 +1058,13 @@ Reports:
|
|
|
1058
1058
|
Gateway RSS/heap headroom, host free-memory reserve, direct child processes, and known heavy local daemons such as ComfyUI or Unity when process listing is available.
|
|
1059
1059
|
|
|
1060
1060
|
Environment:
|
|
1061
|
-
PIBO_GATEWAY_RESOURCE_GUARD=warn|block
|
|
1061
|
+
PIBO_GATEWAY_RESOURCE_GUARD=block|warn|off (default: block)
|
|
1062
1062
|
PIBO_GATEWAY_MIN_FREE_MEMORY_BYTES=<bytes>
|
|
1063
1063
|
PIBO_GATEWAY_MIN_HEAP_AVAILABLE_BYTES=<bytes>
|
|
1064
1064
|
PIBO_GATEWAY_MAX_RSS_BYTES=<bytes>
|
|
1065
1065
|
PIBO_GATEWAY_KNOWN_DAEMON_WARNING_RSS_BYTES=<bytes>
|
|
1066
|
+
PIBO_GATEWAY_MAX_CONCURRENT_YIELDED_RUNS=<count> (default: 1)
|
|
1067
|
+
PIBO_GATEWAY_YIELDED_RUN_MEMORY_RESERVATION_BYTES=<bytes> (default: 2147483648)
|
|
1066
1068
|
|
|
1067
1069
|
Next:
|
|
1068
1070
|
pibo debug resources --json
|
package/dist/gateway/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createServer } from "node:net";
|
|
2
2
|
import { createDefaultPiboPluginRegistry, createPiboProfileFromRegistryOrDefault, resolvePiboProfileNameFromRegistryOrDefault } from "../plugins/builtin.js";
|
|
3
3
|
import { PiboSessionRouter } from "../core/session-router.js";
|
|
4
|
+
import { createLoopMessagePreflight } from "../loops/store.js";
|
|
4
5
|
import { loadPiboModelDefaults, selectRequestedModelProfile } from "../core/model-defaults.js";
|
|
5
6
|
import { ResourceReaperService } from "../resources/reaper.js";
|
|
6
7
|
import { DEFAULT_GATEWAY_HOST, DEFAULT_GATEWAY_PORT, encodeFrame, errorResponse, isGatewayRequestFrame, isGatewaySubscribeFrame, } from "./protocol.js";
|
|
@@ -147,6 +148,7 @@ export class PiboGatewayServer {
|
|
|
147
148
|
persistSession: this.options.persistSession,
|
|
148
149
|
pluginRegistry: this.pluginRegistry,
|
|
149
150
|
sessionStore: this.sessionStore,
|
|
151
|
+
messagePreflight: createLoopMessagePreflight({ path: this.options.loopStorePath }),
|
|
150
152
|
});
|
|
151
153
|
this.unsubscribe = this.router.subscribe((event) => this.broadcastRouterEvent(event));
|
|
152
154
|
this.server = createServer((socket) => this.handleSocket(socket));
|
package/dist/gateway/web.js
CHANGED
|
@@ -197,6 +197,7 @@ export async function runWebGatewayServer(options = {}) {
|
|
|
197
197
|
...resolvedOptions,
|
|
198
198
|
pluginRegistry,
|
|
199
199
|
resourceReaper: resolveGatewayResourceReaperOptions(resolvedOptions),
|
|
200
|
+
loopStorePath: resolvedOptions.chat?.ralphStorePath,
|
|
200
201
|
});
|
|
201
202
|
await server.start();
|
|
202
203
|
}
|
package/dist/loops/accounting.js
CHANGED
|
@@ -13,7 +13,14 @@ export function goalElapsedWallClockSeconds(job, now = new Date()) {
|
|
|
13
13
|
export function goalRemainingTokens(job) {
|
|
14
14
|
return job.tokenBudget === undefined ? undefined : Math.max(0, job.tokenBudget - (job.state.tokensUsed ?? 0));
|
|
15
15
|
}
|
|
16
|
-
export function goalCanStartNextTurn(job) {
|
|
16
|
+
export function goalCanStartNextTurn(job, now = new Date()) {
|
|
17
|
+
if (job.mode !== 'goal' || !job.enabled)
|
|
18
|
+
return false;
|
|
19
|
+
const status = job.state.goalStatus ?? 'paused';
|
|
20
|
+
if (status !== 'active')
|
|
21
|
+
return false;
|
|
22
|
+
if (job.state.nextAttemptAt && Date.parse(job.state.nextAttemptAt) > now.getTime())
|
|
23
|
+
return false;
|
|
17
24
|
const remaining = goalRemainingTokens(job);
|
|
18
25
|
return remaining === undefined || remaining > (job.tokenReserve ?? 0);
|
|
19
26
|
}
|
package/dist/loops/cli.js
CHANGED
|
@@ -207,7 +207,7 @@ export async function runLoopCli(argv = process.argv, defaults = {}) {
|
|
|
207
207
|
printJson(job);
|
|
208
208
|
else
|
|
209
209
|
console.log(`${job.id}\tcancel-requested\t${job.name}`); store.close(); });
|
|
210
|
-
program.command('remove').argument('<id>').description('Delete a Loop job').option('--json', 'Print JSON').action((id, options) => { const store = createDefaultPiboLoopStore({ path: program.opts().store }); const removed = store.removeJob(id); if (options.json)
|
|
210
|
+
program.command('remove').argument('<id>').description('Delete a Loop job and all of its runs and facts; active runs must be cancelled first').option('--json', 'Print JSON').action((id, options) => { const store = createDefaultPiboLoopStore({ path: program.opts().store }); const removed = store.removeJob(id); if (options.json)
|
|
211
211
|
printJson({ removed });
|
|
212
212
|
else
|
|
213
213
|
console.log(removed ? 'removed' : 'not found'); store.close(); });
|