@pasko70/pibo 1.11.2 → 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.2.vsix → pibo-vscode-ext-1.11.3.vsix} +0 -0
- 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 +262 -50
- 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/runs/registry.js +19 -0
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ 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
|
}
|
|
@@ -147,8 +158,12 @@ export class PiboSessionRouter {
|
|
|
147
158
|
signalRegistry;
|
|
148
159
|
runtimeRegistry;
|
|
149
160
|
scheduledRunReminders = new Map();
|
|
161
|
+
runReminderGenerations = new Map();
|
|
162
|
+
quiescingSessions = new Set();
|
|
163
|
+
disposingSessions = new Map();
|
|
150
164
|
idleSessionTimers = new Map();
|
|
151
165
|
routedSessionIdleTimeoutMs;
|
|
166
|
+
routedSessionDisposeTimeoutMs;
|
|
152
167
|
baseProfile;
|
|
153
168
|
pluginRegistry;
|
|
154
169
|
sessionStore;
|
|
@@ -176,6 +191,10 @@ export class PiboSessionRouter {
|
|
|
176
191
|
: typeof idleTimeoutMs === "number" && Number.isFinite(idleTimeoutMs) && idleTimeoutMs > 0
|
|
177
192
|
? idleTimeoutMs
|
|
178
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;
|
|
179
198
|
const defaultProfileName = selectDefaultPiboProfileName(this.pluginRegistry);
|
|
180
199
|
this.baseProfile = options.profile ?? createPiboProfileFromRegistryOrDefault(this.pluginRegistry, defaultProfileName);
|
|
181
200
|
this.reliabilityStore = options.reliabilityStore ?? (options.persistSession === false ? undefined : createDefaultPiboReliabilityStore());
|
|
@@ -196,6 +215,16 @@ export class PiboSessionRouter {
|
|
|
196
215
|
async emit(event) {
|
|
197
216
|
if (this.closing)
|
|
198
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
|
+
}
|
|
199
228
|
if (event.type === "message" && event.id) {
|
|
200
229
|
const stored = this.sessionStore.get(event.piboSessionId);
|
|
201
230
|
if (stored)
|
|
@@ -221,6 +250,9 @@ export class PiboSessionRouter {
|
|
|
221
250
|
throw error;
|
|
222
251
|
}
|
|
223
252
|
this.clearIdleSessionTimer(event.piboSessionId);
|
|
253
|
+
let teardownCompleted = false;
|
|
254
|
+
if (teardownAction)
|
|
255
|
+
this.beginSessionQuiescence(teardownIds);
|
|
224
256
|
try {
|
|
225
257
|
if (event.type === "message") {
|
|
226
258
|
return event.delivery === "steer"
|
|
@@ -233,12 +265,23 @@ export class PiboSessionRouter {
|
|
|
233
265
|
else if (event.action === "dispose" || event.action === "kill" || event.action === "kill_all") {
|
|
234
266
|
this.signalRegistry.project({ type: "session_disposed", piboSessionId: event.piboSessionId, reason: `${event.action} action` });
|
|
235
267
|
}
|
|
236
|
-
const output = await session.executeAction(event);
|
|
237
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);
|
|
238
277
|
await this.disposeSessionSubtree(event.piboSessionId, "dispose action", { cancelRuns: true });
|
|
278
|
+
teardownCompleted = true;
|
|
279
|
+
return output;
|
|
239
280
|
}
|
|
240
|
-
|
|
281
|
+
const output = await session.executeAction(event);
|
|
282
|
+
if (event.action === "kill" || event.action === "kill_all") {
|
|
241
283
|
await this.disposeSessionSubtree(event.piboSessionId, `${event.action} action`, { cancelRuns: event.action === "kill_all" });
|
|
284
|
+
teardownCompleted = true;
|
|
242
285
|
}
|
|
243
286
|
else if (shouldResetSessionAfterAction(event.action)) {
|
|
244
287
|
await this.resetCachedSession(event.piboSessionId, "provider auth changed");
|
|
@@ -246,6 +289,9 @@ export class PiboSessionRouter {
|
|
|
246
289
|
return output;
|
|
247
290
|
}
|
|
248
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
|
+
}
|
|
249
295
|
if (event.type === "message" && event.id) {
|
|
250
296
|
this.signalRegistry.project({
|
|
251
297
|
type: "message_rejected",
|
|
@@ -267,46 +313,118 @@ export class PiboSessionRouter {
|
|
|
267
313
|
}
|
|
268
314
|
}
|
|
269
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);
|
|
270
321
|
const killed = [];
|
|
271
322
|
const cancelledRuns = [];
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
this.
|
|
275
|
-
|
|
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
|
+
}
|
|
276
335
|
if (options?.includeRuns) {
|
|
277
|
-
const runs = this.runRegistry.cancelControllerRuns(
|
|
278
|
-
cancelledRuns.push(...runs.map((
|
|
336
|
+
const runs = this.runRegistry.cancelControllerRuns(id);
|
|
337
|
+
cancelledRuns.push(...runs.map((run) => run.runId));
|
|
279
338
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
cancelledRuns.push(...children.cancelledRuns);
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
283
341
|
await this.disposeSessionSubtree(piboSessionId, "kill", { cancelRuns: false });
|
|
284
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}"`);
|
|
285
348
|
return { killed, cancelledRuns };
|
|
286
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
|
+
}
|
|
287
372
|
async disposeSessionSubtree(piboSessionId, reason, options) {
|
|
288
373
|
const ids = [piboSessionId, ...this.descendantSessionIds(piboSessionId)];
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
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)
|
|
292
380
|
this.runRegistry.cancelControllerRuns(id);
|
|
293
|
-
this.clearIdleSessionTimer(id);
|
|
294
|
-
this.scheduledRunReminders.delete(id);
|
|
295
|
-
const cached = this.sessions.get(id);
|
|
296
|
-
if (cached)
|
|
297
|
-
sessions.push(cached);
|
|
298
|
-
this.sessions.delete(id);
|
|
299
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?.();
|
|
300
416
|
try {
|
|
301
|
-
await
|
|
302
|
-
await Promise.all(sessions.map((session) => session.dispose()));
|
|
417
|
+
await operation;
|
|
303
418
|
}
|
|
304
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
|
+
}
|
|
305
426
|
await this.telemetryWriter?.flush();
|
|
306
427
|
}
|
|
307
|
-
for (const id of ids) {
|
|
308
|
-
this.signalRegistry.project({ type: "session_disposed", piboSessionId: id, reason });
|
|
309
|
-
}
|
|
310
428
|
}
|
|
311
429
|
descendantSessionIds(parentId) {
|
|
312
430
|
const output = [];
|
|
@@ -457,18 +575,28 @@ export class PiboSessionRouter {
|
|
|
457
575
|
}
|
|
458
576
|
async disposeAllUnsafe() {
|
|
459
577
|
try {
|
|
578
|
+
const initialIds = [...new Set([...this.sessions.keys(), ...this.pendingSessions.keys()])];
|
|
579
|
+
this.beginSessionQuiescence(initialIds);
|
|
460
580
|
await Promise.allSettled([...this.pendingSessions.values()]);
|
|
461
|
-
const sessions = [...this.sessions.
|
|
462
|
-
this.sessions.clear();
|
|
581
|
+
const sessions = [...this.sessions.entries()];
|
|
463
582
|
for (const timer of this.idleSessionTimers.values())
|
|
464
583
|
clearTimeout(timer);
|
|
465
584
|
this.idleSessionTimers.clear();
|
|
466
585
|
this.runRegistry.cancelAll("Pibo session router was disposed.");
|
|
467
|
-
for (const session of sessions)
|
|
468
|
-
this.signalRegistry.project({ type: "session_disposed", piboSessionId: session.getStatus().piboSessionId, reason: "router disposed" });
|
|
469
586
|
this.scheduledRunReminders.clear();
|
|
470
|
-
await this.runtimeRegistry.closeAll({ force: true });
|
|
471
|
-
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");
|
|
472
600
|
}
|
|
473
601
|
finally {
|
|
474
602
|
await this.telemetryWriter?.dispose();
|
|
@@ -519,6 +647,14 @@ export class PiboSessionRouter {
|
|
|
519
647
|
async getOrCreateSession(piboSessionId) {
|
|
520
648
|
if (this.closing)
|
|
521
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
|
+
}
|
|
522
658
|
const existing = this.sessions.get(piboSessionId);
|
|
523
659
|
if (existing) {
|
|
524
660
|
this.clearIdleSessionTimer(piboSessionId);
|
|
@@ -538,6 +674,7 @@ export class PiboSessionRouter {
|
|
|
538
674
|
}
|
|
539
675
|
async createRoutedSession(piboSessionId) {
|
|
540
676
|
const piboSession = this.resolvePiboSession(piboSessionId);
|
|
677
|
+
let session;
|
|
541
678
|
this.signalRegistry.project({ type: "session_created", session: piboSession });
|
|
542
679
|
const profile = createPiboProfileFromRegistryOrDefault(this.pluginRegistry, piboSession.profile);
|
|
543
680
|
const parentPiSessionId = piboSession.parentId
|
|
@@ -569,10 +706,11 @@ export class PiboSessionRouter {
|
|
|
569
706
|
piboSessionId: piboSession.id,
|
|
570
707
|
piboRoomId: piboRoomIdFromMetadata(piboSession.metadata),
|
|
571
708
|
timezone: userSettings.timezone,
|
|
709
|
+
getActiveMessage: () => session?.getActiveMessage(),
|
|
572
710
|
},
|
|
573
711
|
});
|
|
574
712
|
const initialFastMode = resolvePiboSessionInitialFastMode(piboSession) ?? selectRequestedFastMode(profileForSession(profile, piboSession.piSessionId, parentPiSessionId), modelDefaults) ?? false;
|
|
575
|
-
|
|
713
|
+
session = new RoutedSession(piboSession.id, runtime, this.emitOutput, this.pluginRegistry, this.options.forwardPiEvents ?? false, this.telemetryRecorder
|
|
576
714
|
? (id, event, context) => this.telemetryRecorder?.recordPiEvent(id, event, { session: this.sessionStore.get(id), status: context.status, activeEventId: context.activeEventId })
|
|
577
715
|
: undefined, initialFastMode, (result, event) => this.handleSessionOperation(result, event), (id, opts) => this.killChildSessions(id, opts), (state) => {
|
|
578
716
|
this.signalRegistry.project({ type: "session_processing_changed", piboSessionId: piboSession.id, processing: state.processing, queuedMessages: state.queuedMessages });
|
|
@@ -583,7 +721,7 @@ export class PiboSessionRouter {
|
|
|
583
721
|
}, (messages, reason) => this.telemetryRecorder?.recordMessagesInterrupted(messages, {
|
|
584
722
|
session: this.sessionStore.get(piboSession.id),
|
|
585
723
|
status: this.sessions.get(piboSession.id)?.getStatus(),
|
|
586
|
-
}, reason));
|
|
724
|
+
}, reason), this.options.messagePreflight);
|
|
587
725
|
this.sessions.set(piboSession.id, session);
|
|
588
726
|
return session;
|
|
589
727
|
}
|
|
@@ -641,14 +779,42 @@ export class PiboSessionRouter {
|
|
|
641
779
|
});
|
|
642
780
|
}
|
|
643
781
|
async resetCachedSession(piboSessionId, reason) {
|
|
644
|
-
const
|
|
782
|
+
const existingDisposal = this.disposingSessions.get(piboSessionId);
|
|
783
|
+
if (existingDisposal)
|
|
784
|
+
await existingDisposal;
|
|
645
785
|
this.clearIdleSessionTimer(piboSessionId);
|
|
646
|
-
|
|
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?.();
|
|
647
812
|
try {
|
|
648
|
-
await
|
|
649
|
-
await cached?.dispose();
|
|
813
|
+
await operation;
|
|
650
814
|
}
|
|
651
815
|
finally {
|
|
816
|
+
if (this.disposingSessions.get(piboSessionId) === operation)
|
|
817
|
+
this.disposingSessions.delete(piboSessionId);
|
|
652
818
|
await this.telemetryWriter?.flush();
|
|
653
819
|
}
|
|
654
820
|
if (reason)
|
|
@@ -699,6 +865,7 @@ export class PiboSessionRouter {
|
|
|
699
865
|
return {
|
|
700
866
|
startToolRun: ({ toolName, params, completionPolicy, retryable, maxAttempts, timeoutMs, serviceWarning, execute }) => {
|
|
701
867
|
const admission = this.gatewayWorkAdmission.reserve(`yielded run ${toolName}`);
|
|
868
|
+
const reminderGeneration = this.runReminderGeneration(parentPiboSessionId);
|
|
702
869
|
let run;
|
|
703
870
|
try {
|
|
704
871
|
run = this.runRegistry.startToolRun({
|
|
@@ -721,7 +888,7 @@ export class PiboSessionRouter {
|
|
|
721
888
|
const result = await execute();
|
|
722
889
|
const completed = this.runRegistry.complete(run.runId, result);
|
|
723
890
|
if (completed)
|
|
724
|
-
this.
|
|
891
|
+
this.handleTerminalRunReminder(parentPiboSessionId, completed.runId, reminderGeneration);
|
|
725
892
|
}
|
|
726
893
|
catch (error) {
|
|
727
894
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -729,7 +896,7 @@ export class PiboSessionRouter {
|
|
|
729
896
|
? this.runRegistry.timeOut(run.runId, message, error.timeoutPhase)
|
|
730
897
|
: this.runRegistry.fail(run.runId, message);
|
|
731
898
|
if (terminalRun)
|
|
732
|
-
this.
|
|
899
|
+
this.handleTerminalRunReminder(parentPiboSessionId, terminalRun.runId, reminderGeneration);
|
|
733
900
|
}
|
|
734
901
|
finally {
|
|
735
902
|
admission.release();
|
|
@@ -742,9 +909,8 @@ export class PiboSessionRouter {
|
|
|
742
909
|
waitForRun: (runId, timeoutMs) => this.runRegistry.wait(parentPiboSessionId, runId, timeoutMs),
|
|
743
910
|
readRun: (runId) => {
|
|
744
911
|
const run = this.runRegistry.read(parentPiboSessionId, runId);
|
|
745
|
-
if (run.consumed && isTerminalRunStatus(run.status))
|
|
912
|
+
if (run.consumed && isTerminalRunStatus(run.status))
|
|
746
913
|
this.refreshQueuedRunReminders(parentPiboSessionId);
|
|
747
|
-
}
|
|
748
914
|
return run;
|
|
749
915
|
},
|
|
750
916
|
cancelRun: async (runId) => {
|
|
@@ -857,17 +1023,54 @@ export class PiboSessionRouter {
|
|
|
857
1023
|
}
|
|
858
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 });
|
|
859
1025
|
}
|
|
860
|
-
|
|
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;
|
|
861
1061
|
if (!this.runRegistry.hasPendingNotification(piboSessionId, { includeAlreadyNotified }))
|
|
862
1062
|
return;
|
|
863
1063
|
const previous = this.scheduledRunReminders.get(piboSessionId);
|
|
864
|
-
if (previous
|
|
865
|
-
this.scheduledRunReminders.set(piboSessionId,
|
|
1064
|
+
if (previous?.generation === expectedGeneration) {
|
|
1065
|
+
this.scheduledRunReminders.set(piboSessionId, {
|
|
1066
|
+
generation: expectedGeneration,
|
|
1067
|
+
includeAlreadyNotified: previous.includeAlreadyNotified || includeAlreadyNotified,
|
|
1068
|
+
});
|
|
866
1069
|
return;
|
|
867
1070
|
}
|
|
868
|
-
this.scheduledRunReminders.set(piboSessionId, includeAlreadyNotified);
|
|
1071
|
+
this.scheduledRunReminders.set(piboSessionId, { generation: expectedGeneration, includeAlreadyNotified });
|
|
869
1072
|
queueMicrotask(() => {
|
|
870
|
-
void this.deliverRunReminder(piboSessionId);
|
|
1073
|
+
void this.deliverRunReminder(piboSessionId, expectedGeneration);
|
|
871
1074
|
});
|
|
872
1075
|
}
|
|
873
1076
|
refreshQueuedRunReminders(piboSessionId) {
|
|
@@ -875,23 +1078,32 @@ export class PiboSessionRouter {
|
|
|
875
1078
|
if (removed > 0)
|
|
876
1079
|
this.scheduleRunReminder(piboSessionId, true);
|
|
877
1080
|
}
|
|
878
|
-
async deliverRunReminder(piboSessionId) {
|
|
879
|
-
const
|
|
1081
|
+
async deliverRunReminder(piboSessionId, expectedGeneration) {
|
|
1082
|
+
const scheduled = this.scheduledRunReminders.get(piboSessionId);
|
|
1083
|
+
if (!scheduled || scheduled.generation !== expectedGeneration)
|
|
1084
|
+
return;
|
|
880
1085
|
this.scheduledRunReminders.delete(piboSessionId);
|
|
881
|
-
|
|
1086
|
+
if (this.closing || this.quiescingSessions.has(piboSessionId) || expectedGeneration !== this.runReminderGeneration(piboSessionId))
|
|
1087
|
+
return;
|
|
1088
|
+
const notification = this.runRegistry.createNotification(piboSessionId, { includeAlreadyNotified: scheduled.includeAlreadyNotified });
|
|
882
1089
|
if (!notification)
|
|
883
1090
|
return;
|
|
884
1091
|
try {
|
|
885
1092
|
const session = await this.getOrCreateSession(piboSessionId);
|
|
1093
|
+
if (this.closing || this.quiescingSessions.has(piboSessionId) || expectedGeneration !== this.runReminderGeneration(piboSessionId))
|
|
1094
|
+
return;
|
|
886
1095
|
session.enqueueMessage({
|
|
887
1096
|
type: "message",
|
|
888
1097
|
piboSessionId,
|
|
889
1098
|
text: formatRunReminderMessage(notification),
|
|
890
1099
|
source: "service",
|
|
1100
|
+
capabilityScope: "run-reminder",
|
|
891
1101
|
id: randomUUID(),
|
|
892
1102
|
});
|
|
893
1103
|
}
|
|
894
1104
|
catch (error) {
|
|
1105
|
+
if (this.closing || this.quiescingSessions.has(piboSessionId) || expectedGeneration !== this.runReminderGeneration(piboSessionId))
|
|
1106
|
+
return;
|
|
895
1107
|
const message = error instanceof Error ? error.message : String(error);
|
|
896
1108
|
this.emitOutput({
|
|
897
1109
|
type: "session_error",
|
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(); });
|