@pasko70/pibo 1.7.11 → 1.8.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/dist/core/context-guard.js +172 -5
- package/dist/core/provider-telemetry.js +97 -4
- package/dist/core/routed-session.js +110 -16
- package/dist/core/runtime-telemetry.js +254 -94
- package/dist/core/runtime.js +72 -11
- package/dist/core/session-errors.js +16 -0
- package/dist/core/session-router.js +114 -21
- package/dist/data/telemetry.js +115 -26
- package/dist/gateway/cli.js +35 -16
- package/dist/gateway/pidfile.js +90 -44
- package/dist/gateway/server.js +22 -16
- package/dist/gateway/web.js +29 -23
- package/dist/ralph/service.js +60 -20
- package/dist/ralph/store.js +1 -1
- package/package.json +1 -1
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.7.11.vsix +0 -0
|
@@ -1,7 +1,92 @@
|
|
|
1
1
|
import { DEFAULT_COMPACTION_SETTINGS, buildSessionContext, estimateTokens, } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
export const PIBO_CONTEXT_GUARD_NOTICE = "Context safety interrupted this response before adding it to long-term context. Pibo is compacting the session before continuing.";
|
|
3
|
+
export const PIBO_CONTEXT_GUARD_RESUME_MESSAGE_TYPE = "pibo-context-guard-resume";
|
|
4
|
+
export const PIBO_CONTEXT_GUARD_RESUME_PROMPT = "Continue the interrupted task autonomously from the compacted context. Do not wait for additional user input and do not repeat the context-safety notice.";
|
|
3
5
|
const DEFAULT_MIN_COMPACTION_RESERVE_TOKENS = 1024;
|
|
4
6
|
const FALLBACK_CONTEXT_WINDOW = 0;
|
|
7
|
+
const MAX_CONTEXT_GUARD_RECOVERY_ATTEMPTS = 3;
|
|
8
|
+
const contextGuardRecoveries = new WeakMap();
|
|
9
|
+
export function createPiboAssistantContextGuardRecovery() {
|
|
10
|
+
let pending;
|
|
11
|
+
let claimed = false;
|
|
12
|
+
let cancelError;
|
|
13
|
+
let cancelHandler;
|
|
14
|
+
return {
|
|
15
|
+
begin() {
|
|
16
|
+
if (pending && !pending.settled)
|
|
17
|
+
return;
|
|
18
|
+
cancelError = undefined;
|
|
19
|
+
let resolve;
|
|
20
|
+
const promise = new Promise((done) => {
|
|
21
|
+
resolve = done;
|
|
22
|
+
});
|
|
23
|
+
pending = { promise, resolve, settled: false };
|
|
24
|
+
},
|
|
25
|
+
complete() {
|
|
26
|
+
if (!pending || pending.settled)
|
|
27
|
+
return;
|
|
28
|
+
pending.settled = true;
|
|
29
|
+
pending.resolve({ ok: true });
|
|
30
|
+
},
|
|
31
|
+
fail(error) {
|
|
32
|
+
if (!pending || pending.settled)
|
|
33
|
+
return;
|
|
34
|
+
pending.settled = true;
|
|
35
|
+
pending.resolve({ ok: false, error });
|
|
36
|
+
},
|
|
37
|
+
cancel(error) {
|
|
38
|
+
cancelError = error;
|
|
39
|
+
if (pending && !pending.settled) {
|
|
40
|
+
pending.settled = true;
|
|
41
|
+
pending.resolve({ ok: false, error });
|
|
42
|
+
}
|
|
43
|
+
cancelHandler?.();
|
|
44
|
+
},
|
|
45
|
+
claim() {
|
|
46
|
+
claimed = true;
|
|
47
|
+
},
|
|
48
|
+
isClaimed() {
|
|
49
|
+
return claimed;
|
|
50
|
+
},
|
|
51
|
+
isPending() {
|
|
52
|
+
return pending !== undefined && !pending.settled;
|
|
53
|
+
},
|
|
54
|
+
onCancel(handler) {
|
|
55
|
+
cancelHandler = handler;
|
|
56
|
+
},
|
|
57
|
+
async wait() {
|
|
58
|
+
const current = pending;
|
|
59
|
+
if (!current)
|
|
60
|
+
return false;
|
|
61
|
+
const outcome = await current.promise;
|
|
62
|
+
const cancelled = cancelError;
|
|
63
|
+
if (pending === current) {
|
|
64
|
+
pending = undefined;
|
|
65
|
+
cancelError = undefined;
|
|
66
|
+
}
|
|
67
|
+
if (cancelled)
|
|
68
|
+
throw cancelled;
|
|
69
|
+
if (!outcome.ok)
|
|
70
|
+
throw outcome.error;
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function registerPiboAssistantContextGuardRecovery(session, recovery) {
|
|
76
|
+
contextGuardRecoveries.set(session, recovery);
|
|
77
|
+
}
|
|
78
|
+
export function claimPiboAssistantContextGuardRecovery(session) {
|
|
79
|
+
contextGuardRecoveries.get(session)?.claim();
|
|
80
|
+
}
|
|
81
|
+
export function cancelPiboAssistantContextGuardRecovery(session, error) {
|
|
82
|
+
contextGuardRecoveries.get(session)?.cancel(error);
|
|
83
|
+
}
|
|
84
|
+
export function isPiboAssistantContextGuardRecoveryPending(session) {
|
|
85
|
+
return contextGuardRecoveries.get(session)?.isPending() ?? false;
|
|
86
|
+
}
|
|
87
|
+
export async function waitForPiboAssistantContextGuardRecovery(session) {
|
|
88
|
+
return await contextGuardRecoveries.get(session)?.wait() ?? false;
|
|
89
|
+
}
|
|
5
90
|
function textTokensByLength(length) {
|
|
6
91
|
return Math.ceil(length / 4);
|
|
7
92
|
}
|
|
@@ -79,15 +164,43 @@ function compactionInstructions(projection) {
|
|
|
79
164
|
: "";
|
|
80
165
|
return `Pibo interrupted the previous assistant response before persisting the full output because it would exceed the safe context budget.${usage} Summarize the durable conversation up to the guard notice, preserve the user's current task and important recent facts, and leave enough context budget for the next response.`;
|
|
81
166
|
}
|
|
82
|
-
export function createPiboAssistantContextGuardExtension(options = {}) {
|
|
167
|
+
export function createPiboAssistantContextGuardExtension(options = {}, recovery = createPiboAssistantContextGuardRecovery()) {
|
|
83
168
|
return (pi) => {
|
|
84
|
-
const state = {
|
|
169
|
+
const state = {
|
|
170
|
+
deltaChars: 0,
|
|
171
|
+
tripped: false,
|
|
172
|
+
compactQueued: false,
|
|
173
|
+
resumeRequested: false,
|
|
174
|
+
resumeInProgress: false,
|
|
175
|
+
resumeSettling: false,
|
|
176
|
+
recoveryAttempts: 0,
|
|
177
|
+
};
|
|
85
178
|
function resetAssistantState() {
|
|
86
179
|
state.deltaChars = 0;
|
|
87
180
|
state.tripped = false;
|
|
88
181
|
state.compactQueued = false;
|
|
89
182
|
state.lastProjection = undefined;
|
|
90
183
|
}
|
|
184
|
+
function clearResumeStartTimer() {
|
|
185
|
+
if (state.resumeStartTimer === undefined)
|
|
186
|
+
return;
|
|
187
|
+
clearTimeout(state.resumeStartTimer);
|
|
188
|
+
state.resumeStartTimer = undefined;
|
|
189
|
+
}
|
|
190
|
+
function resetRecoveryState() {
|
|
191
|
+
clearResumeStartTimer();
|
|
192
|
+
state.tripped = false;
|
|
193
|
+
state.compactQueued = false;
|
|
194
|
+
state.resumeRequested = false;
|
|
195
|
+
state.resumeInProgress = false;
|
|
196
|
+
state.resumeSettling = false;
|
|
197
|
+
state.recoveryAttempts = 0;
|
|
198
|
+
}
|
|
199
|
+
function failRecovery(error) {
|
|
200
|
+
resetRecoveryState();
|
|
201
|
+
recovery.fail(error);
|
|
202
|
+
}
|
|
203
|
+
recovery.onCancel(resetRecoveryState);
|
|
91
204
|
function tripIfNeeded(ctx, assistantTokens) {
|
|
92
205
|
const projection = projectAssistantContextGuard(ctx, assistantTokens, options);
|
|
93
206
|
state.lastProjection = projection;
|
|
@@ -96,6 +209,20 @@ export function createPiboAssistantContextGuardExtension(options = {}) {
|
|
|
96
209
|
state.tripped = true;
|
|
97
210
|
return true;
|
|
98
211
|
}
|
|
212
|
+
pi.on("agent_start", () => {
|
|
213
|
+
if (!state.resumeRequested)
|
|
214
|
+
return;
|
|
215
|
+
clearResumeStartTimer();
|
|
216
|
+
state.resumeRequested = false;
|
|
217
|
+
state.resumeInProgress = true;
|
|
218
|
+
});
|
|
219
|
+
pi.on("agent_settled", () => {
|
|
220
|
+
if (!state.resumeSettling)
|
|
221
|
+
return;
|
|
222
|
+
state.resumeSettling = false;
|
|
223
|
+
state.recoveryAttempts = 0;
|
|
224
|
+
recovery.complete();
|
|
225
|
+
});
|
|
99
226
|
pi.on("message_start", (event) => {
|
|
100
227
|
if (event.message.role === "assistant")
|
|
101
228
|
resetAssistantState();
|
|
@@ -117,10 +244,50 @@ export function createPiboAssistantContextGuardExtension(options = {}) {
|
|
|
117
244
|
return { message: replacementAssistantMessage(event.message) };
|
|
118
245
|
});
|
|
119
246
|
pi.on("agent_end", (_event, ctx) => {
|
|
120
|
-
if (
|
|
247
|
+
if (state.tripped && !state.compactQueued) {
|
|
248
|
+
if (state.recoveryAttempts >= MAX_CONTEXT_GUARD_RECOVERY_ATTEMPTS) {
|
|
249
|
+
failRecovery(new Error(`Context guard recovery exceeded ${MAX_CONTEXT_GUARD_RECOVERY_ATTEMPTS} compaction attempts`));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
state.compactQueued = true;
|
|
253
|
+
state.recoveryAttempts++;
|
|
254
|
+
recovery.begin();
|
|
255
|
+
let callbackHandled = false;
|
|
256
|
+
ctx.compact({
|
|
257
|
+
customInstructions: compactionInstructions(state.lastProjection),
|
|
258
|
+
onComplete: () => {
|
|
259
|
+
if (callbackHandled || !recovery.isPending())
|
|
260
|
+
return;
|
|
261
|
+
callbackHandled = true;
|
|
262
|
+
state.tripped = false;
|
|
263
|
+
state.compactQueued = false;
|
|
264
|
+
state.resumeRequested = true;
|
|
265
|
+
if (recovery.isClaimed()) {
|
|
266
|
+
recovery.complete();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
state.resumeStartTimer = setTimeout(() => {
|
|
270
|
+
failRecovery(new Error("Context guard continuation did not start"));
|
|
271
|
+
}, 5000);
|
|
272
|
+
pi.sendMessage({
|
|
273
|
+
customType: PIBO_CONTEXT_GUARD_RESUME_MESSAGE_TYPE,
|
|
274
|
+
content: [{ type: "text", text: PIBO_CONTEXT_GUARD_RESUME_PROMPT }],
|
|
275
|
+
display: false,
|
|
276
|
+
}, { triggerTurn: true });
|
|
277
|
+
},
|
|
278
|
+
onError: (error) => {
|
|
279
|
+
if (callbackHandled || !recovery.isPending())
|
|
280
|
+
return;
|
|
281
|
+
callbackHandled = true;
|
|
282
|
+
failRecovery(error);
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (!state.resumeInProgress)
|
|
121
288
|
return;
|
|
122
|
-
state.
|
|
123
|
-
|
|
289
|
+
state.resumeInProgress = false;
|
|
290
|
+
state.resumeSettling = true;
|
|
124
291
|
});
|
|
125
292
|
};
|
|
126
293
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { BestEffortTelemetryService, } from "../data/telemetry.js";
|
|
3
|
+
import { normalizeSessionErrorDetails } from "./session-errors.js";
|
|
3
4
|
export class PiboProviderTelemetryRecorder {
|
|
4
5
|
options;
|
|
5
6
|
telemetry;
|
|
@@ -16,6 +17,9 @@ export class PiboProviderTelemetryRecorder {
|
|
|
16
17
|
if (!turn)
|
|
17
18
|
return undefined;
|
|
18
19
|
const now = options.at ?? new Date().toISOString();
|
|
20
|
+
if (this.activeProviderRequest()) {
|
|
21
|
+
this.finishActiveProviderRequest("aborted", now, "provider request superseded", undefined, "provider_superseded");
|
|
22
|
+
}
|
|
19
23
|
const model = modelInfo(options.model, this.options.model, payload);
|
|
20
24
|
const providerRequestId = `pr_${randomUUID()}`;
|
|
21
25
|
const phaseId = this.nextPhaseId(turn.turnId, "provider_request");
|
|
@@ -83,7 +87,7 @@ export class PiboProviderTelemetryRecorder {
|
|
|
83
87
|
lastProgressAt: now,
|
|
84
88
|
summary: "provider response received",
|
|
85
89
|
});
|
|
86
|
-
const streamPhaseId =
|
|
90
|
+
const streamPhaseId = providerStreamPhaseId(request);
|
|
87
91
|
this.telemetry.upsertPhase({
|
|
88
92
|
phaseId: streamPhaseId,
|
|
89
93
|
turnId: request.turnId,
|
|
@@ -122,6 +126,56 @@ export class PiboProviderTelemetryRecorder {
|
|
|
122
126
|
return undefined;
|
|
123
127
|
}
|
|
124
128
|
}
|
|
129
|
+
// The assistant message boundary ends the provider stream even when the wider
|
|
130
|
+
// Pibo turn continues with a long-running tool or another provider request.
|
|
131
|
+
recordMessageEnd(message, options = {}) {
|
|
132
|
+
if (!this.options.store || !isAssistantMessage(message))
|
|
133
|
+
return undefined;
|
|
134
|
+
const status = providerStatusForMessage(message);
|
|
135
|
+
const summary = providerSummaryForStatus(status);
|
|
136
|
+
const errorMessage = safeMessageError(message);
|
|
137
|
+
const errorDetails = status === "error"
|
|
138
|
+
? normalizeSessionErrorDetails(errorMessage ?? "Provider request failed.", {
|
|
139
|
+
api: optionalString(message.api),
|
|
140
|
+
provider: optionalString(message.provider),
|
|
141
|
+
model: optionalString(message.model),
|
|
142
|
+
})
|
|
143
|
+
: undefined;
|
|
144
|
+
return this.finishActiveProviderRequest(status, options.at ?? new Date().toISOString(), summary, errorMessage, errorDetails?.category ?? errorDetails?.errorClass);
|
|
145
|
+
}
|
|
146
|
+
recordShutdown(reason, at = new Date().toISOString()) {
|
|
147
|
+
return this.finishActiveProviderRequest("aborted", at, reason, undefined, "runtime_abort");
|
|
148
|
+
}
|
|
149
|
+
finishActiveProviderRequest(status, now, summary, errorMessage, errorCategory) {
|
|
150
|
+
if (!this.options.store)
|
|
151
|
+
return undefined;
|
|
152
|
+
try {
|
|
153
|
+
const request = this.activeProviderRequest();
|
|
154
|
+
if (!request)
|
|
155
|
+
return undefined;
|
|
156
|
+
const phaseStatus = status === "completed" ? "ok" : status;
|
|
157
|
+
const requestPhaseId = request.phaseId ?? `${request.turnId}:provider_request`;
|
|
158
|
+
if (this.options.store.getPhase(requestPhaseId)?.status === "open") {
|
|
159
|
+
this.telemetry.finishPhase(requestPhaseId, { status: phaseStatus, endedAt: now, lastProgressAt: now, summary });
|
|
160
|
+
}
|
|
161
|
+
const streamPhaseId = providerStreamPhaseId(request);
|
|
162
|
+
if (this.options.store.getPhase(streamPhaseId)?.status === "open") {
|
|
163
|
+
this.telemetry.finishPhase(streamPhaseId, { status: phaseStatus, endedAt: now, lastProgressAt: now, summary });
|
|
164
|
+
}
|
|
165
|
+
const updated = this.updateProviderRequest(request, {
|
|
166
|
+
status,
|
|
167
|
+
completedAt: now,
|
|
168
|
+
errorCategory: status === "error" ? errorCategory ?? "provider_error" : status === "aborted" ? errorCategory ?? "runtime_abort" : undefined,
|
|
169
|
+
errorMessage,
|
|
170
|
+
});
|
|
171
|
+
this.activeProviderRequestId = undefined;
|
|
172
|
+
return updated;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
this.options.onError?.(error);
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
125
179
|
activeTurn() {
|
|
126
180
|
const detail = this.options.store?.getSessionTelemetry(this.options.session.id, { limit: 10 });
|
|
127
181
|
const active = detail?.activeTurn;
|
|
@@ -138,11 +192,10 @@ export class PiboProviderTelemetryRecorder {
|
|
|
138
192
|
const turn = this.activeTurn();
|
|
139
193
|
if (!turn)
|
|
140
194
|
return undefined;
|
|
141
|
-
|
|
142
|
-
return [...(timeline?.providerRequests ?? [])].reverse().find((request) => !isTerminalProviderStatus(request.status));
|
|
195
|
+
return this.options.store?.getActiveProviderRequestForTurn(turn.turnId);
|
|
143
196
|
}
|
|
144
197
|
nextPhaseId(turnId, phaseName) {
|
|
145
|
-
const count = this.options.store?.
|
|
198
|
+
const count = this.options.store?.countPhasesForTurn(turnId, phaseName) ?? 0;
|
|
146
199
|
return count === 0 ? `${turnId}:${phaseName}` : `${turnId}:${phaseName}:${count + 1}`;
|
|
147
200
|
}
|
|
148
201
|
updateProviderRequest(existing, input) {
|
|
@@ -162,12 +215,18 @@ export class PiboProviderTelemetryRecorder {
|
|
|
162
215
|
responseHeadersAt: input.responseHeadersAt,
|
|
163
216
|
firstByteAt: input.firstByteAt,
|
|
164
217
|
httpStatus: input.httpStatus,
|
|
218
|
+
completedAt: input.completedAt,
|
|
165
219
|
rawEventCount: existing.rawEventCount,
|
|
166
220
|
normalizedEventCount: existing.normalizedEventCount,
|
|
167
221
|
parseErrorCount: existing.parseErrorCount,
|
|
168
222
|
unknownEventCount: existing.unknownEventCount,
|
|
169
223
|
bytesReceived: existing.bytesReceived,
|
|
170
224
|
eventTypeCounts: existing.eventTypeCounts,
|
|
225
|
+
eventStreamId: existing.eventStreamId,
|
|
226
|
+
eventId: existing.eventId,
|
|
227
|
+
payloadRef: existing.payloadRef,
|
|
228
|
+
errorCategory: input.errorCategory,
|
|
229
|
+
errorMessage: input.errorMessage,
|
|
171
230
|
captureMode: existing.captureMode,
|
|
172
231
|
retentionClass: existing.retentionClass,
|
|
173
232
|
});
|
|
@@ -182,11 +241,45 @@ export function createPiboProviderTelemetryExtension(options) {
|
|
|
182
241
|
pi.on("after_provider_response", (event) => {
|
|
183
242
|
recorder.recordResponse({ status: event.status, headers: event.headers });
|
|
184
243
|
});
|
|
244
|
+
pi.on("message_end", (event) => {
|
|
245
|
+
recorder.recordMessageEnd(event.message);
|
|
246
|
+
});
|
|
247
|
+
pi.on("session_shutdown", (event) => {
|
|
248
|
+
recorder.recordShutdown(`provider session shutdown: ${event.reason}`);
|
|
249
|
+
});
|
|
185
250
|
};
|
|
186
251
|
}
|
|
187
252
|
export function isTerminalProviderStatus(status) {
|
|
188
253
|
return status === "completed" || status === "error" || status === "aborted" || status === "timeout";
|
|
189
254
|
}
|
|
255
|
+
function isAssistantMessage(message) {
|
|
256
|
+
return Boolean(message && typeof message === "object" && message.role === "assistant");
|
|
257
|
+
}
|
|
258
|
+
function providerStatusForMessage(message) {
|
|
259
|
+
if (message.stopReason === "error")
|
|
260
|
+
return "error";
|
|
261
|
+
if (message.stopReason === "aborted")
|
|
262
|
+
return "aborted";
|
|
263
|
+
return "completed";
|
|
264
|
+
}
|
|
265
|
+
function providerSummaryForStatus(status) {
|
|
266
|
+
if (status === "error")
|
|
267
|
+
return "provider stream failed";
|
|
268
|
+
if (status === "aborted")
|
|
269
|
+
return "provider stream aborted";
|
|
270
|
+
return "provider stream completed";
|
|
271
|
+
}
|
|
272
|
+
function providerStreamPhaseId(request) {
|
|
273
|
+
return `${request.turnId}:provider_stream:${request.providerRequestId}`;
|
|
274
|
+
}
|
|
275
|
+
function safeMessageError(message) {
|
|
276
|
+
return typeof message.errorMessage === "string" && message.errorMessage.trim().length > 0
|
|
277
|
+
? message.errorMessage.replace(/\s+/g, " ").trim().slice(0, 512)
|
|
278
|
+
: undefined;
|
|
279
|
+
}
|
|
280
|
+
function optionalString(value) {
|
|
281
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
282
|
+
}
|
|
190
283
|
function modelInfo(primary, fallback, payload) {
|
|
191
284
|
const payloadModel = modelIdFromPayload(payload);
|
|
192
285
|
return {
|
|
@@ -2,6 +2,7 @@ import { SessionManager, shouldCompact } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { getOpenAiCodexProviderUsageForActiveModel } from "../auth/openai-codex-usage.js";
|
|
3
3
|
import { normalizeSessionErrorDetails, runtimeSessionErrorDetails } from "./session-errors.js";
|
|
4
4
|
import { expandInlineSkills } from "./skill-expansion.js";
|
|
5
|
+
import { PIBO_CONTEXT_GUARD_RESUME_MESSAGE_TYPE, PIBO_CONTEXT_GUARD_RESUME_PROMPT, cancelPiboAssistantContextGuardRecovery, claimPiboAssistantContextGuardRecovery, waitForPiboAssistantContextGuardRecovery, } from "./context-guard.js";
|
|
5
6
|
const FAST_SERVICE_TIER = "priority";
|
|
6
7
|
function modelSupportsFastServiceTier(model) {
|
|
7
8
|
if (!model)
|
|
@@ -21,6 +22,9 @@ function withFastServiceTierOption(options) {
|
|
|
21
22
|
function errorMessage(error) {
|
|
22
23
|
return error instanceof Error ? error.message : String(error);
|
|
23
24
|
}
|
|
25
|
+
function isAssistantMessage(message) {
|
|
26
|
+
return Boolean(message && typeof message === "object" && message.role === "assistant");
|
|
27
|
+
}
|
|
24
28
|
function numberValue(value) {
|
|
25
29
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
26
30
|
}
|
|
@@ -388,6 +392,7 @@ export class RoutedSession {
|
|
|
388
392
|
onSessionOperation;
|
|
389
393
|
onKillChildren;
|
|
390
394
|
onStateChange;
|
|
395
|
+
onMessagesInterrupted;
|
|
391
396
|
queue = [];
|
|
392
397
|
processing = false;
|
|
393
398
|
disposed = false;
|
|
@@ -398,9 +403,12 @@ export class RoutedSession {
|
|
|
398
403
|
nextAssistantIndex = 0;
|
|
399
404
|
activeThinkingIndex;
|
|
400
405
|
nextThinkingIndex = 0;
|
|
406
|
+
pendingAssistantError;
|
|
407
|
+
activeMessageFailed = false;
|
|
401
408
|
unsubscribe;
|
|
409
|
+
recoverySession;
|
|
402
410
|
isContinuePatched = false;
|
|
403
|
-
constructor(piboSessionId, runtime, emit, pluginRegistry, forwardPiEvents, onPiEventTelemetry, initialFastMode, onSessionOperation, onKillChildren, onStateChange) {
|
|
411
|
+
constructor(piboSessionId, runtime, emit, pluginRegistry, forwardPiEvents, onPiEventTelemetry, initialFastMode, onSessionOperation, onKillChildren, onStateChange, onMessagesInterrupted) {
|
|
404
412
|
this.piboSessionId = piboSessionId;
|
|
405
413
|
this.runtime = runtime;
|
|
406
414
|
this.emit = emit;
|
|
@@ -410,6 +418,7 @@ export class RoutedSession {
|
|
|
410
418
|
this.onSessionOperation = onSessionOperation;
|
|
411
419
|
this.onKillChildren = onKillChildren;
|
|
412
420
|
this.onStateChange = onStateChange;
|
|
421
|
+
this.onMessagesInterrupted = onMessagesInterrupted;
|
|
413
422
|
this.fastMode = initialFastMode && this.fastModeSupported();
|
|
414
423
|
this.bindRuntimeSession();
|
|
415
424
|
this.patchFastModeProviderRequest();
|
|
@@ -485,19 +494,44 @@ export class RoutedSession {
|
|
|
485
494
|
}
|
|
486
495
|
bindRuntimeSession() {
|
|
487
496
|
this.unsubscribe?.();
|
|
488
|
-
|
|
497
|
+
const session = this.runtime.session;
|
|
498
|
+
if (this.recoverySession && this.recoverySession !== session) {
|
|
499
|
+
cancelPiboAssistantContextGuardRecovery(this.recoverySession, new Error("Context guard recovery cancelled because the Pi session changed"));
|
|
500
|
+
}
|
|
501
|
+
this.recoverySession = session;
|
|
502
|
+
claimPiboAssistantContextGuardRecovery(session);
|
|
503
|
+
this.unsubscribe = session.subscribe((event) => {
|
|
489
504
|
this.onPiEventTelemetry?.(this.piboSessionId, event, { status: this.getStatus(), activeEventId: this.activeMessage?.id });
|
|
490
505
|
const model = this.runtime.session.model;
|
|
491
506
|
const normalized = normalizePiEvent(this.piboSessionId, event, { contextWindow: numberValue(model?.contextWindow) });
|
|
492
|
-
|
|
493
|
-
|
|
507
|
+
const candidate = event && typeof event === "object" ? event : undefined;
|
|
508
|
+
const assistantMessageEnded = candidate?.type === "message_end" && isAssistantMessage(candidate.message);
|
|
509
|
+
// Pi may recover an assistant error through retry or compaction. Publish it only
|
|
510
|
+
// after agent_settled confirms that no automatic continuation remains.
|
|
511
|
+
if (assistantMessageEnded && normalized?.type === "session_error") {
|
|
512
|
+
this.pendingAssistantError = this.withActiveMessage(normalized);
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
if (assistantMessageEnded)
|
|
516
|
+
this.pendingAssistantError = undefined;
|
|
517
|
+
if (normalized)
|
|
518
|
+
this.emit(this.withActiveMessage(normalized));
|
|
494
519
|
}
|
|
520
|
+
if (candidate?.type === "agent_settled")
|
|
521
|
+
this.flushPendingAssistantError();
|
|
495
522
|
if (this.forwardPiEvents) {
|
|
496
523
|
this.emit({ type: "pi_event", piboSessionId: this.piboSessionId, event });
|
|
497
524
|
}
|
|
498
525
|
this.handleCompactionEvent(event);
|
|
499
526
|
});
|
|
500
527
|
}
|
|
528
|
+
flushPendingAssistantError() {
|
|
529
|
+
if (!this.pendingAssistantError)
|
|
530
|
+
return;
|
|
531
|
+
this.activeMessageFailed = true;
|
|
532
|
+
this.emit(this.pendingAssistantError);
|
|
533
|
+
this.pendingAssistantError = undefined;
|
|
534
|
+
}
|
|
501
535
|
handleCompactionEvent(event) {
|
|
502
536
|
if (!event || typeof event !== "object")
|
|
503
537
|
return;
|
|
@@ -565,6 +599,7 @@ export class RoutedSession {
|
|
|
565
599
|
getStatus() {
|
|
566
600
|
const enabledTools = this.runtime.session.getActiveToolNames();
|
|
567
601
|
const thinkingLevel = this.runtime.session.thinkingLevel;
|
|
602
|
+
const settingsManager = this.runtime.session.settingsManager;
|
|
568
603
|
return {
|
|
569
604
|
piboSessionId: this.piboSessionId,
|
|
570
605
|
queuedMessages: this.queue.length,
|
|
@@ -576,6 +611,12 @@ export class RoutedSession {
|
|
|
576
611
|
disposed: this.disposed,
|
|
577
612
|
thinkingLevel,
|
|
578
613
|
fastMode: this.getFastModeResult().mode === "fast",
|
|
614
|
+
...(settingsManager ? {
|
|
615
|
+
retry: {
|
|
616
|
+
...settingsManager.getRetrySettings(),
|
|
617
|
+
provider: settingsManager.getProviderRetrySettings(),
|
|
618
|
+
},
|
|
619
|
+
} : {}),
|
|
579
620
|
};
|
|
580
621
|
}
|
|
581
622
|
getContextUsage() {
|
|
@@ -595,15 +636,17 @@ export class RoutedSession {
|
|
|
595
636
|
}
|
|
596
637
|
removeQueuedMessages(predicate) {
|
|
597
638
|
this.assertActive();
|
|
598
|
-
|
|
639
|
+
const removedMessages = [];
|
|
599
640
|
for (let index = this.queue.length - 1; index >= 0; index -= 1) {
|
|
600
641
|
const item = this.queue[index];
|
|
601
642
|
if (item.kind !== "message" || !predicate(item.event))
|
|
602
643
|
continue;
|
|
603
644
|
this.queue.splice(index, 1);
|
|
604
|
-
|
|
645
|
+
removedMessages.push(item.event);
|
|
605
646
|
}
|
|
606
|
-
|
|
647
|
+
removedMessages.reverse();
|
|
648
|
+
this.notifyMessagesInterrupted(removedMessages, "queued message removed");
|
|
649
|
+
return removedMessages.length;
|
|
607
650
|
}
|
|
608
651
|
getCurrentSession() {
|
|
609
652
|
return this.createSessionSnapshot();
|
|
@@ -726,16 +769,23 @@ export class RoutedSession {
|
|
|
726
769
|
async dispose() {
|
|
727
770
|
if (this.disposed)
|
|
728
771
|
return;
|
|
772
|
+
this.notifyMessagesInterrupted(this.activeAndQueuedMessages(), "session disposed");
|
|
729
773
|
this.queue.length = 0;
|
|
730
774
|
this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: true });
|
|
731
775
|
this.unsubscribe?.();
|
|
732
776
|
this.unsubscribe = undefined;
|
|
777
|
+
if (this.recoverySession) {
|
|
778
|
+
this.cancelContextGuardRecovery("Context guard recovery cancelled because the routed session was disposed");
|
|
779
|
+
this.recoverySession = undefined;
|
|
780
|
+
}
|
|
733
781
|
this.disposed = true;
|
|
734
782
|
await this.runtime.dispose();
|
|
735
783
|
}
|
|
736
784
|
async kill() {
|
|
785
|
+
this.notifyMessagesInterrupted(this.activeAndQueuedMessages(), "session killed");
|
|
737
786
|
this.queue.length = 0;
|
|
738
787
|
this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: this.disposed });
|
|
788
|
+
this.cancelContextGuardRecovery("Context guard recovery cancelled because the routed session was killed");
|
|
739
789
|
await this.runtime.session.abort();
|
|
740
790
|
return this.piboSessionId;
|
|
741
791
|
}
|
|
@@ -743,11 +793,15 @@ export class RoutedSession {
|
|
|
743
793
|
this.assertActive();
|
|
744
794
|
const queuedIndex = this.queue.findIndex((item) => item.event.id === eventId);
|
|
745
795
|
if (queuedIndex >= 0) {
|
|
746
|
-
this.queue.splice(queuedIndex, 1);
|
|
796
|
+
const [removed] = this.queue.splice(queuedIndex, 1);
|
|
797
|
+
if (removed?.kind === "message")
|
|
798
|
+
this.notifyMessagesInterrupted([removed.event], "message cancelled");
|
|
747
799
|
this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: this.disposed });
|
|
748
800
|
return true;
|
|
749
801
|
}
|
|
750
802
|
if (this.activeMessage?.id === eventId) {
|
|
803
|
+
this.notifyMessagesInterrupted([this.activeMessage], "message cancelled");
|
|
804
|
+
this.cancelContextGuardRecovery("Context guard recovery cancelled with the active message");
|
|
751
805
|
await this.runtime.session.abort();
|
|
752
806
|
return true;
|
|
753
807
|
}
|
|
@@ -785,18 +839,37 @@ export class RoutedSession {
|
|
|
785
839
|
});
|
|
786
840
|
try {
|
|
787
841
|
this.activeMessage = event;
|
|
842
|
+
this.pendingAssistantError = undefined;
|
|
843
|
+
this.activeMessageFailed = false;
|
|
788
844
|
this.activeAssistantIndex = undefined;
|
|
789
845
|
this.nextAssistantIndex = 0;
|
|
790
846
|
this.activeThinkingIndex = undefined;
|
|
791
847
|
this.nextThinkingIndex = 0;
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
848
|
+
const session = this.runtime.session;
|
|
849
|
+
const expandedText = expandInlineSkills(event.text, session.resourceLoader.getSkills().skills);
|
|
850
|
+
await session.prompt(expandedText, { source: promptSource(event.source) });
|
|
851
|
+
while (await waitForPiboAssistantContextGuardRecovery(session)) {
|
|
852
|
+
try {
|
|
853
|
+
await session.sendCustomMessage({
|
|
854
|
+
customType: PIBO_CONTEXT_GUARD_RESUME_MESSAGE_TYPE,
|
|
855
|
+
content: [{ type: "text", text: PIBO_CONTEXT_GUARD_RESUME_PROMPT }],
|
|
856
|
+
display: false,
|
|
857
|
+
}, { triggerTurn: true });
|
|
858
|
+
}
|
|
859
|
+
catch (error) {
|
|
860
|
+
const resumeError = error instanceof Error ? error : new Error(String(error));
|
|
861
|
+
cancelPiboAssistantContextGuardRecovery(session, resumeError);
|
|
862
|
+
throw resumeError;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (!this.activeMessageFailed) {
|
|
866
|
+
this.emit({
|
|
867
|
+
type: "message_finished",
|
|
868
|
+
piboSessionId: this.piboSessionId,
|
|
869
|
+
eventId: event.id,
|
|
870
|
+
source: event.source,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
800
873
|
}
|
|
801
874
|
catch (error) {
|
|
802
875
|
const message = errorMessage(error);
|
|
@@ -810,6 +883,8 @@ export class RoutedSession {
|
|
|
810
883
|
}
|
|
811
884
|
finally {
|
|
812
885
|
this.activeMessage = undefined;
|
|
886
|
+
this.pendingAssistantError = undefined;
|
|
887
|
+
this.activeMessageFailed = false;
|
|
813
888
|
this.activeAssistantIndex = undefined;
|
|
814
889
|
this.nextAssistantIndex = 0;
|
|
815
890
|
this.activeThinkingIndex = undefined;
|
|
@@ -866,6 +941,9 @@ export class RoutedSession {
|
|
|
866
941
|
getProviderUsage: () => this.getProviderUsage(),
|
|
867
942
|
clearQueue: () => this.clearQueue(),
|
|
868
943
|
abort: async () => {
|
|
944
|
+
if (this.activeMessage)
|
|
945
|
+
this.notifyMessagesInterrupted([this.activeMessage], "abort requested");
|
|
946
|
+
this.cancelContextGuardRecovery("Context guard recovery cancelled by abort");
|
|
869
947
|
await this.runtime.session.abort();
|
|
870
948
|
},
|
|
871
949
|
dispose: () => this.dispose(),
|
|
@@ -906,6 +984,11 @@ export class RoutedSession {
|
|
|
906
984
|
},
|
|
907
985
|
}, event);
|
|
908
986
|
}
|
|
987
|
+
cancelContextGuardRecovery(message) {
|
|
988
|
+
const session = this.recoverySession ?? this.runtime.session;
|
|
989
|
+
cancelPiboAssistantContextGuardRecovery(session, new Error(message));
|
|
990
|
+
session.abortCompaction?.();
|
|
991
|
+
}
|
|
909
992
|
assertActive() {
|
|
910
993
|
if (this.disposed) {
|
|
911
994
|
throw new Error(`Session "${this.piboSessionId}" has been disposed`);
|
|
@@ -927,10 +1010,21 @@ export class RoutedSession {
|
|
|
927
1010
|
}
|
|
928
1011
|
clearQueue() {
|
|
929
1012
|
const cleared = this.queue.length;
|
|
1013
|
+
const removedMessages = this.queue.flatMap((item) => item.kind === "message" ? [item.event] : []);
|
|
930
1014
|
this.queue.length = 0;
|
|
1015
|
+
this.notifyMessagesInterrupted(removedMessages, "queue cleared");
|
|
931
1016
|
this.onStateChange?.({ processing: this.processing, queuedMessages: this.queue.length, disposed: this.disposed });
|
|
932
1017
|
return cleared;
|
|
933
1018
|
}
|
|
1019
|
+
activeAndQueuedMessages() {
|
|
1020
|
+
const messages = this.queue.flatMap((item) => item.kind === "message" ? [item.event] : []);
|
|
1021
|
+
return this.activeMessage ? [this.activeMessage, ...messages] : messages;
|
|
1022
|
+
}
|
|
1023
|
+
notifyMessagesInterrupted(messages, reason) {
|
|
1024
|
+
if (messages.length === 0)
|
|
1025
|
+
return;
|
|
1026
|
+
this.onMessagesInterrupted?.(messages, reason);
|
|
1027
|
+
}
|
|
934
1028
|
createSessionSnapshot() {
|
|
935
1029
|
const session = this.runtime.session;
|
|
936
1030
|
const manager = session.sessionManager;
|