@pasko70/pibo 1.7.12 → 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.
@@ -1,16 +1,26 @@
1
1
  import { BestEffortTelemetryService, } from "../data/telemetry.js";
2
2
  import { isTerminalProviderStatus } from "./provider-telemetry.js";
3
3
  const TERMINAL_TURN_STATUSES = new Set(["ok", "error", "aborted", "timeout"]);
4
+ const DEFAULT_PROGRESS_FLUSH_INTERVAL_MS = 1_000;
4
5
  export class PiboRuntimeTelemetryRecorder {
5
6
  store;
6
7
  onError;
7
8
  telemetry;
8
9
  providerEventMode;
10
+ progressFlushIntervalMs;
11
+ pendingProviderProgress = new Map();
12
+ providerRequestCache = new Map();
13
+ lastProviderFlushAtMs = new Map();
14
+ lastProgressWriteAtMs = new Map();
9
15
  constructor(store, onError, options = {}) {
10
16
  this.store = store;
11
17
  this.onError = onError;
12
18
  this.telemetry = new BestEffortTelemetryService(store, onError);
13
19
  this.providerEventMode = options.providerEventMode ?? "aggregate";
20
+ const progressFlushIntervalMs = options.progressFlushIntervalMs;
21
+ this.progressFlushIntervalMs = typeof progressFlushIntervalMs === "number" && Number.isFinite(progressFlushIntervalMs) && progressFlushIntervalMs >= 0
22
+ ? progressFlushIntervalMs
23
+ : DEFAULT_PROGRESS_FLUSH_INTERVAL_MS;
14
24
  }
15
25
  recordOutput(event, context = {}) {
16
26
  if (!this.store)
@@ -32,6 +42,15 @@ export class PiboRuntimeTelemetryRecorder {
32
42
  this.onError?.(error);
33
43
  }
34
44
  }
45
+ recordMessagesInterrupted(messages, context = {}, reason = "message interrupted") {
46
+ if (!this.store)
47
+ return;
48
+ for (const message of messages) {
49
+ if (!message.id)
50
+ continue;
51
+ this.recordTurnTerminal({ piboSessionId: message.piboSessionId, eventId: message.id }, context, "aborted", "abort", reason, "runtime_abort");
52
+ }
53
+ }
35
54
  recordOutputUnsafe(event, context) {
36
55
  switch (event.type) {
37
56
  case "message_queued":
@@ -44,15 +63,19 @@ export class PiboRuntimeTelemetryRecorder {
44
63
  this.recordMessageFinished(event, context);
45
64
  return;
46
65
  case "assistant_delta":
47
- case "assistant_message":
48
66
  this.recordProviderStreamProgress(event, context, "assistant_text", "assistant text progress");
49
67
  return;
68
+ case "assistant_message":
69
+ this.recordProviderStreamProgress(event, context, "assistant_text", "assistant text progress", true);
70
+ return;
50
71
  case "thinking_started":
72
+ this.recordProviderStreamProgress(event, context, "reasoning", "reasoning progress", true);
73
+ return;
51
74
  case "thinking_delta":
52
75
  this.recordProviderStreamProgress(event, context, "reasoning", "reasoning progress");
53
76
  return;
54
77
  case "thinking_finished": {
55
- this.recordProviderStreamProgress(event, context, "reasoning", "reasoning finished");
78
+ this.recordProviderStreamProgress(event, context, "reasoning", "reasoning finished", true);
56
79
  const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, undefined, context) ?? this.activeTurnContext(event.piboSessionId, context);
57
80
  this.finishOpenPhasesByName(turn?.turnId, "reasoning", "ok");
58
81
  return;
@@ -61,15 +84,19 @@ export class PiboRuntimeTelemetryRecorder {
61
84
  this.recordToolArgsPhase(event, context);
62
85
  return;
63
86
  case "tool_execution_started":
87
+ this.recordToolExecutionPhase(event, context, true);
88
+ return;
64
89
  case "tool_execution_updated":
65
- this.recordToolExecutionPhase(event, context);
90
+ this.recordToolExecutionPhase(event, context, false);
66
91
  return;
67
92
  case "tool_execution_finished":
68
93
  this.recordToolExecutionFinished(event, context);
69
94
  return;
70
- case "session_error":
71
- this.recordTurnTerminal(event, context, "error", "error", event.error, event.errorDetails?.category ?? event.errorDetails?.errorClass);
95
+ case "session_error": {
96
+ const status = terminalTurnStatusForSessionError(event);
97
+ this.recordTurnTerminal(event, context, status, status === "aborted" ? "abort" : status === "timeout" ? "timeout" : "error", event.error, event.errorDetails?.category ?? event.errorDetails?.errorClass);
72
98
  return;
99
+ }
73
100
  case "execution_result":
74
101
  this.recordExecutionResult(event, context);
75
102
  return;
@@ -82,43 +109,52 @@ export class PiboRuntimeTelemetryRecorder {
82
109
  if (!summary)
83
110
  return;
84
111
  const turn = context.activeEventId
85
- ? this.turnContextForEvent(piboSessionId, context.activeEventId, undefined, context)
112
+ ? this.progressTurnContextForEvent(piboSessionId, context.activeEventId, context)
86
113
  : this.activeTurnContext(piboSessionId, context);
87
114
  if (!turn)
88
115
  return;
89
- const providerRequest = this.activeProviderRequestForTurn(turn.turnId);
90
- if (!providerRequest)
91
- return;
92
116
  const now = new Date().toISOString();
93
- if (summary.upstreamResponseId && summary.upstreamResponseId !== providerRequest.upstreamResponseId) {
94
- this.upsertProviderRequestFromExisting(providerRequest, { upstreamResponseId: summary.upstreamResponseId });
117
+ if (summary.assistantEventType === "start") {
118
+ this.flushProviderProgress(turn.turnId, now, { force: true, includeLatest: true });
119
+ this.clearProviderProgress(turn.turnId);
95
120
  }
96
- const providerEventInput = {
97
- providerRequestId: providerRequest.providerRequestId,
98
- piboSessionId: turn.piboSessionId,
99
- turnId: turn.turnId,
100
- phaseId: providerRequest.phaseId,
101
- receivedAt: now,
102
- eventType: summary.eventType,
103
- byteSize: summary.byteSize,
104
- parseStatus: summary.parseStatus,
105
- normalizedType: summary.normalizedType,
106
- normalizedEventDelta: 0,
107
- eventId: turn.eventId,
108
- itemId: summary.itemId,
109
- toolCallId: summary.toolCallId,
110
- safeFields: summary.safeFields,
111
- };
112
- if (this.providerEventMode === "detailed") {
113
- this.telemetry.appendProviderEventSummary(providerEventInput);
121
+ const providerRequest = this.providerEventMode === "detailed"
122
+ ? this.providerRequestForTurn(turn.turnId, { includeLatest: summary.messageEnded, refresh: summary.messageEnded })
123
+ : this.accumulateProviderEvent(turn, summary, now);
124
+ if (!providerRequest) {
125
+ if (summary.messageEnded)
126
+ this.clearProviderProgress(turn.turnId);
127
+ return;
114
128
  }
115
- else {
116
- this.telemetry.recordProviderEventSummary(providerEventInput);
129
+ if (this.providerEventMode === "detailed") {
130
+ this.telemetry.appendProviderEventSummary({
131
+ providerRequestId: providerRequest.providerRequestId,
132
+ piboSessionId: turn.piboSessionId,
133
+ turnId: turn.turnId,
134
+ phaseId: providerRequest.phaseId,
135
+ receivedAt: now,
136
+ eventType: summary.eventType,
137
+ byteSize: summary.byteSize,
138
+ parseStatus: summary.parseStatus,
139
+ normalizedType: summary.normalizedType,
140
+ normalizedEventDelta: 0,
141
+ eventId: turn.eventId,
142
+ itemId: summary.itemId,
143
+ toolCallId: summary.toolCallId,
144
+ safeFields: summary.safeFields,
145
+ });
117
146
  }
118
147
  if (summary.toolCallId && summary.assistantEventType?.startsWith("toolcall_")) {
119
- this.recordPiToolCallProgress(turn, providerRequest.providerRequestId, summary, now);
148
+ const forceToolProgress = summary.assistantEventType !== "toolcall_delta";
149
+ if (this.shouldPersistProgress(`${turn.turnId}:tool_args:${summary.toolCallId}`, forceToolProgress)) {
150
+ this.recordPiToolCallProgress(turn, providerRequest.providerRequestId, summary, now);
151
+ }
152
+ }
153
+ if (!summary.messageEnded && !isTerminalProviderStatus(providerRequest.status) && !summary.normalizedType && this.shouldPersistProgress(`${turn.turnId}:provider_stream:${providerRequest.providerRequestId}`)) {
154
+ this.startOrProgressPhase(turn, "provider_stream", now, "provider event metadata", { providerRequestId: providerRequest.providerRequestId });
120
155
  }
121
- this.startOrProgressPhase(turn, "provider_stream", now, "provider event metadata", { providerRequestId: providerRequest.providerRequestId });
156
+ if (summary.messageEnded)
157
+ this.clearProviderProgress(turn.turnId);
122
158
  }
123
159
  recordMessageQueued(event, context) {
124
160
  const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, event.source, context);
@@ -197,23 +233,32 @@ export class PiboRuntimeTelemetryRecorder {
197
233
  this.recordTurnTerminal(event, context, "ok", "finish", "message finished");
198
234
  }
199
235
  recordExecutionResult(event, context) {
200
- if (event.action === "abort" || event.action === "dispose" || event.action === "kill_all") {
236
+ if (event.action === "abort" || event.action === "dispose" || event.action === "kill" || event.action === "kill_all") {
201
237
  this.recordTurnTerminal(event, context, "aborted", "abort", `${event.action} requested`);
202
238
  return;
203
239
  }
204
- if (event.action === "clear_queue") {
205
- this.recordTurnProgress(event, context, "queued", "clear_queue requested");
206
- }
207
240
  }
208
- recordProviderStreamProgress(event, context, phaseName, summary) {
209
- const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, undefined, context) ?? this.activeTurnContext(event.piboSessionId, context);
241
+ recordProviderStreamProgress(event, context, phaseName, summary, force = false) {
242
+ const turn = event.eventId
243
+ ? this.progressTurnContextForEvent(event.piboSessionId, event.eventId, context)
244
+ : this.activeTurnContext(event.piboSessionId, context);
210
245
  if (!turn)
211
246
  return;
212
247
  const now = new Date().toISOString();
213
- const providerRequest = this.activeProviderRequestForTurn(turn.turnId);
248
+ const providerRequest = this.accumulateNormalizedProviderProgress(turn, now, force);
249
+ const progressKey = `${turn.turnId}:${phaseName}:${providerRequest?.providerRequestId ?? "none"}`;
250
+ if (!this.shouldPersistProgress(progressKey, force))
251
+ return;
252
+ const storedTurn = this.store?.getTurn(turn.turnId);
253
+ if (storedTurn && TERMINAL_TURN_STATUSES.has(storedTurn.status)) {
254
+ this.clearTurnProgress(turn.turnId);
255
+ return;
256
+ }
214
257
  this.closeOpenPhasesByName(turn.turnId, "message_started", "ok", now);
215
- this.progressProviderRequest(providerRequest, now);
216
- this.startOrProgressPhase(turn, "provider_stream", now, "normalized provider stream progress", { providerRequestId: providerRequest?.providerRequestId });
258
+ const providerStreamKey = `${turn.turnId}:provider_stream:${providerRequest?.providerRequestId ?? "none"}`;
259
+ if ((!providerRequest || !isTerminalProviderStatus(providerRequest.status)) && this.shouldPersistProgress(providerStreamKey, force)) {
260
+ this.startOrProgressPhase(turn, "provider_stream", now, "normalized provider stream progress", { providerRequestId: providerRequest?.providerRequestId });
261
+ }
217
262
  this.startOrProgressPhase(turn, phaseName, now, summary, { updateTurn: true, providerRequestId: providerRequest?.providerRequestId });
218
263
  }
219
264
  recordToolArgsPhase(event, context) {
@@ -224,8 +269,8 @@ export class PiboRuntimeTelemetryRecorder {
224
269
  this.closeOpenPhasesByName(turn.turnId, "message_started", "ok", now);
225
270
  this.closeOpenPhasesByName(turn.turnId, "assistant_text", "ok", now);
226
271
  this.closeOpenPhasesByName(turn.turnId, "reasoning", "ok", now);
227
- const providerRequest = this.activeProviderRequestForTurn(turn.turnId) ?? this.latestProviderRequestForTurn(turn.turnId);
228
- this.progressProviderRequest(providerRequest && !isTerminalProviderStatus(providerRequest.status) ? providerRequest : undefined, now);
272
+ const providerRequest = this.accumulateNormalizedProviderProgress(turn, now, true)
273
+ ?? this.providerRequestForTurn(turn.turnId, { includeLatest: true });
229
274
  this.upsertToolCallArgs(turn, {
230
275
  toolCallId: event.toolCallId,
231
276
  toolName: event.toolName,
@@ -244,10 +289,19 @@ export class PiboRuntimeTelemetryRecorder {
244
289
  this.telemetry.finishPhase(phase.phaseId, { status: "ok", endedAt: now, lastProgressAt: now, summary: "tool arguments complete" });
245
290
  }
246
291
  }
247
- recordToolExecutionPhase(event, context) {
248
- const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, undefined, context) ?? this.activeTurnContext(event.piboSessionId, context);
292
+ recordToolExecutionPhase(event, context, force) {
293
+ const turn = event.eventId
294
+ ? this.progressTurnContextForEvent(event.piboSessionId, event.eventId, context)
295
+ : this.activeTurnContext(event.piboSessionId, context);
249
296
  if (!turn)
250
297
  return;
298
+ if (!this.shouldPersistProgress(`${turn.turnId}:tool_execution:${event.toolCallId}`, force))
299
+ return;
300
+ const storedTurn = this.store?.getTurn(turn.turnId);
301
+ if (storedTurn && TERMINAL_TURN_STATUSES.has(storedTurn.status)) {
302
+ this.clearTurnProgress(turn.turnId);
303
+ return;
304
+ }
251
305
  const now = new Date().toISOString();
252
306
  const existing = this.store?.getToolCall(event.toolCallId);
253
307
  const args = toolArgsMetadata(event.args, true);
@@ -362,17 +416,17 @@ export class PiboRuntimeTelemetryRecorder {
362
416
  eventId: turn.eventId,
363
417
  });
364
418
  }
365
- recordTurnProgress(event, context, phaseName, summary) {
366
- const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, undefined, context) ?? this.activeTurnContext(event.piboSessionId, context);
367
- if (!turn)
368
- return;
369
- this.startOrProgressPhase(turn, phaseName, new Date().toISOString(), summary, { updateTurn: true });
370
- }
371
419
  recordTurnTerminal(event, context, status, phaseName, summary, errorCategory) {
372
- const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, undefined, context) ?? this.activeTurnContext(event.piboSessionId, context);
420
+ const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, undefined, context, { includeTerminal: true }) ?? this.activeTurnContext(event.piboSessionId, context);
373
421
  if (!turn)
374
422
  return;
423
+ const existingTurn = this.store?.getTurn(turn.turnId);
424
+ if (existingTurn && TERMINAL_TURN_STATUSES.has(existingTurn.status)) {
425
+ this.clearTurnProgress(turn.turnId);
426
+ return;
427
+ }
375
428
  const now = new Date().toISOString();
429
+ this.flushProviderProgress(turn.turnId, now, { force: true, includeLatest: true });
376
430
  this.finishOpenPhases(turn.turnId, terminalPhaseStatus(status), now);
377
431
  this.finishActiveProviderRequests(turn.turnId, providerStatusForTurnStatus(status), now, summary, errorCategory);
378
432
  this.finishActiveToolCalls(turn.turnId, status, now, summary);
@@ -403,8 +457,12 @@ export class PiboRuntimeTelemetryRecorder {
403
457
  queueDepth: turn.queueDepth,
404
458
  summary: safeSummary(summary),
405
459
  });
460
+ this.clearTurnProgress(turn.turnId);
406
461
  }
407
462
  startOrProgressPhase(turn, phaseName, now, summary, options = {}) {
463
+ const storedTurn = this.store?.getTurn(turn.turnId);
464
+ if (storedTurn && TERMINAL_TURN_STATUSES.has(storedTurn.status))
465
+ return undefined;
408
466
  const existing = this.openPhaseByName(turn.turnId, phaseName);
409
467
  const phase = this.telemetry.upsertPhase({
410
468
  phaseId: existing?.phaseId ?? this.nextPhaseId(turn.turnId, phaseName),
@@ -438,22 +496,20 @@ export class PiboRuntimeTelemetryRecorder {
438
496
  return phase;
439
497
  }
440
498
  openPhaseByName(turnId, phaseName) {
441
- const timeline = this.store?.getTurnTimeline(turnId, { limit: 100 });
442
- return timeline?.phases.find((phase) => phase.name === phaseName && phase.status === "open");
499
+ return this.store?.getOpenPhaseForTurn(turnId, phaseName);
443
500
  }
444
501
  nextPhaseId(turnId, phaseName) {
445
502
  const base = phaseId(turnId, phaseName);
446
- const phases = this.store?.getTurnTimeline(turnId, { limit: 100 })?.phases.filter((phase) => phase.name === phaseName) ?? [];
447
- if (phases.length === 0)
503
+ const count = this.store?.countPhasesForTurn(turnId, phaseName) ?? 0;
504
+ if (count === 0)
448
505
  return base;
449
- return `${base}:${phases.length + 1}`;
506
+ return `${base}:${count + 1}`;
450
507
  }
451
508
  finishOpenPhasesByName(turnId, phaseName, status, now = new Date().toISOString(), summary) {
452
509
  if (!turnId)
453
510
  return;
454
- const timeline = this.store?.getTurnTimeline(turnId, { limit: 100 });
455
- for (const phase of timeline?.phases ?? []) {
456
- if (phase.name !== phaseName || phase.status !== "open")
511
+ for (const phase of this.store?.listOpenPhasesForTurn(turnId) ?? []) {
512
+ if (phase.name !== phaseName)
457
513
  continue;
458
514
  this.telemetry.finishPhase(phase.phaseId, { status, endedAt: now, lastProgressAt: now, summary });
459
515
  }
@@ -462,35 +518,113 @@ export class PiboRuntimeTelemetryRecorder {
462
518
  this.finishOpenPhasesByName(turnId, phaseName, status, now, summary);
463
519
  }
464
520
  finishOpenPhases(turnId, status, now) {
465
- const timeline = this.store?.getTurnTimeline(turnId, { limit: 100 });
466
- for (const phase of timeline?.phases ?? []) {
467
- if (phase.status !== "open")
468
- continue;
521
+ for (const phase of this.store?.listOpenPhasesForTurn(turnId) ?? []) {
469
522
  this.telemetry.finishPhase(phase.phaseId, { status, endedAt: now, lastProgressAt: now });
470
523
  }
471
524
  }
472
525
  activeProviderRequestForTurn(turnId) {
473
- const timeline = this.store?.getTurnTimeline(turnId, { limit: 100 });
474
- return [...(timeline?.providerRequests ?? [])].reverse().find((request) => !isTerminalProviderStatus(request.status));
526
+ return this.store?.getActiveProviderRequestForTurn(turnId);
475
527
  }
476
528
  latestProviderRequestForTurn(turnId) {
477
- const timeline = this.store?.getTurnTimeline(turnId, { limit: 100 });
478
- return timeline?.providerRequests.at(-1);
529
+ return this.store?.getLatestProviderRequestForTurn(turnId);
530
+ }
531
+ providerRequestForTurn(turnId, options = {}) {
532
+ const cached = this.providerRequestCache.get(turnId);
533
+ if (!options.refresh && cached && (options.includeLatest || !isTerminalProviderStatus(cached.status)))
534
+ return cached;
535
+ const request = this.activeProviderRequestForTurn(turnId)
536
+ ?? (options.includeLatest ? this.latestProviderRequestForTurn(turnId) : undefined);
537
+ if (request)
538
+ this.providerRequestCache.set(turnId, request);
539
+ else
540
+ this.providerRequestCache.delete(turnId);
541
+ return request;
542
+ }
543
+ accumulateProviderEvent(turn, summary, now) {
544
+ const pending = this.pendingProviderProgress.get(turn.turnId) ?? emptyPendingProviderProgress();
545
+ pending.lastRawEventAt = now;
546
+ pending.upstreamResponseId = summary.upstreamResponseId ?? pending.upstreamResponseId;
547
+ pending.rawEventCount += 1;
548
+ pending.parseErrorCount += summary.parseStatus === "invalid_json" ? 1 : 0;
549
+ pending.unknownEventCount += summary.parseStatus === "unknown_type" ? 1 : 0;
550
+ pending.bytesReceived += summary.byteSize;
551
+ pending.eventTypeCounts[summary.eventType] = (pending.eventTypeCounts[summary.eventType] ?? 0) + 1;
552
+ this.pendingProviderProgress.set(turn.turnId, pending);
553
+ return this.flushProviderProgress(turn.turnId, now, {
554
+ force: summary.messageEnded,
555
+ includeLatest: summary.messageEnded,
556
+ refresh: summary.messageEnded,
557
+ });
479
558
  }
480
- progressProviderRequest(request, now) {
559
+ accumulateNormalizedProviderProgress(turn, now, force = false) {
560
+ const request = this.providerRequestForTurn(turn.turnId, { includeLatest: force });
481
561
  if (!request)
482
- return;
483
- this.upsertProviderRequestFromExisting(request, {
484
- status: "streaming",
485
- lastNormalizedEventAt: now,
486
- normalizedEventCount: request.normalizedEventCount + 1,
562
+ return undefined;
563
+ const pending = this.pendingProviderProgress.get(turn.turnId) ?? emptyPendingProviderProgress();
564
+ pending.lastNormalizedEventAt = now;
565
+ pending.normalizedEventCount += 1;
566
+ this.pendingProviderProgress.set(turn.turnId, pending);
567
+ const flushNow = force || request.status === "started" || request.status === "headers";
568
+ return this.flushProviderProgress(turn.turnId, now, { force: flushNow, includeLatest: force }) ?? request;
569
+ }
570
+ flushProviderProgress(turnId, now, options = {}) {
571
+ const pending = this.pendingProviderProgress.get(turnId);
572
+ const request = this.providerRequestForTurn(turnId, options);
573
+ if (!pending)
574
+ return request;
575
+ const nowMs = Date.now();
576
+ const lastFlushAtMs = this.lastProviderFlushAtMs.get(turnId);
577
+ if (!options.force && lastFlushAtMs !== undefined && nowMs - lastFlushAtMs < this.progressFlushIntervalMs)
578
+ return request;
579
+ if (!request) {
580
+ const turn = this.store?.getTurn(turnId);
581
+ if (turn && TERMINAL_TURN_STATUSES.has(turn.status))
582
+ this.clearTurnProgress(turnId);
583
+ return undefined;
584
+ }
585
+ const updated = this.telemetry.recordProviderProgress({
586
+ providerRequestId: request.providerRequestId,
587
+ status: pending.normalizedEventCount > 0 ? "streaming" : undefined,
588
+ lastRawEventAt: pending.lastRawEventAt,
589
+ lastNormalizedEventAt: pending.lastNormalizedEventAt,
590
+ upstreamResponseId: pending.upstreamResponseId,
591
+ rawEventCount: pending.rawEventCount,
592
+ normalizedEventCount: pending.normalizedEventCount,
593
+ parseErrorCount: pending.parseErrorCount,
594
+ unknownEventCount: pending.unknownEventCount,
595
+ bytesReceived: pending.bytesReceived,
596
+ eventTypeCounts: pending.eventTypeCounts,
597
+ updatedAt: now,
487
598
  });
599
+ this.pendingProviderProgress.delete(turnId);
600
+ this.lastProviderFlushAtMs.set(turnId, nowMs);
601
+ if (updated)
602
+ this.providerRequestCache.set(turnId, updated);
603
+ return updated ?? request;
604
+ }
605
+ shouldPersistProgress(key, force = false) {
606
+ const nowMs = Date.now();
607
+ const lastWriteAtMs = this.lastProgressWriteAtMs.get(key);
608
+ if (!force && lastWriteAtMs !== undefined && nowMs - lastWriteAtMs < this.progressFlushIntervalMs)
609
+ return false;
610
+ this.lastProgressWriteAtMs.set(key, nowMs);
611
+ return true;
612
+ }
613
+ clearProviderProgress(turnId) {
614
+ this.pendingProviderProgress.delete(turnId);
615
+ this.providerRequestCache.delete(turnId);
616
+ this.lastProviderFlushAtMs.delete(turnId);
617
+ }
618
+ clearTurnProgress(turnId) {
619
+ this.clearProviderProgress(turnId);
620
+ const prefix = `${turnId}:`;
621
+ for (const key of this.lastProgressWriteAtMs.keys()) {
622
+ if (key.startsWith(prefix))
623
+ this.lastProgressWriteAtMs.delete(key);
624
+ }
488
625
  }
489
626
  finishActiveProviderRequests(turnId, status, now, summary, errorCategory) {
490
- const timeline = this.store?.getTurnTimeline(turnId, { limit: 100 });
491
- for (const request of timeline?.providerRequests ?? []) {
492
- if (isTerminalProviderStatus(request.status))
493
- continue;
627
+ for (const request of this.store?.listActiveProviderRequestsForTurn(turnId) ?? []) {
494
628
  this.upsertProviderRequestFromExisting(request, {
495
629
  status,
496
630
  completedAt: now,
@@ -502,10 +636,7 @@ export class PiboRuntimeTelemetryRecorder {
502
636
  finishActiveToolCalls(turnId, status, now, summary) {
503
637
  if (status === "ok")
504
638
  return;
505
- const timeline = this.store?.getTurnTimeline(turnId, { limit: 100 });
506
- for (const toolCall of timeline?.toolCalls ?? []) {
507
- if (isTerminalToolCallStatus(toolCall.status))
508
- continue;
639
+ for (const toolCall of this.store?.listActiveToolCallsForTurn(turnId) ?? []) {
509
640
  const terminalStatus = terminalToolCallStatus(status);
510
641
  this.telemetry.upsertToolCall({
511
642
  toolCallId: toolCall.toolCallId,
@@ -566,20 +697,34 @@ export class PiboRuntimeTelemetryRecorder {
566
697
  retentionClass: request.retentionClass,
567
698
  });
568
699
  }
569
- turnContextForEvent(piboSessionId, eventId, source, context) {
570
- if (!eventId)
571
- return undefined;
572
- const rootSessionId = rootSessionIdFor(context.session, piboSessionId);
700
+ progressTurnContextForEvent(piboSessionId, eventId, context) {
573
701
  return {
574
702
  turnId: turnIdForEvent(eventId),
575
703
  eventId,
576
704
  piboSessionId,
577
- rootSessionId,
705
+ rootSessionId: rootSessionIdFor(context.session, piboSessionId),
578
706
  roomId: roomIdFor(context.session),
579
- source: telemetrySource(source),
707
+ source: "system",
580
708
  queueDepth: context.status?.queuedMessages,
581
709
  };
582
710
  }
711
+ turnContextForEvent(piboSessionId, eventId, source, context, options = {}) {
712
+ if (!eventId)
713
+ return undefined;
714
+ const turnId = turnIdForEvent(eventId);
715
+ const existing = this.store?.getTurn(turnId);
716
+ if (!options.includeTerminal && existing && TERMINAL_TURN_STATUSES.has(existing.status))
717
+ return undefined;
718
+ return {
719
+ turnId,
720
+ eventId,
721
+ piboSessionId,
722
+ rootSessionId: existing?.rootSessionId ?? rootSessionIdFor(context.session, piboSessionId),
723
+ roomId: existing?.roomId ?? roomIdFor(context.session),
724
+ source: existing?.source ?? telemetrySource(source),
725
+ queueDepth: context.status?.queuedMessages ?? existing?.queueDepth,
726
+ };
727
+ }
583
728
  activeTurnContext(piboSessionId, context) {
584
729
  const detail = this.store?.getSessionTelemetry(piboSessionId, { limit: 10 });
585
730
  const active = detail?.activeTurn;
@@ -596,6 +741,16 @@ export class PiboRuntimeTelemetryRecorder {
596
741
  };
597
742
  }
598
743
  }
744
+ function emptyPendingProviderProgress() {
745
+ return {
746
+ rawEventCount: 0,
747
+ normalizedEventCount: 0,
748
+ parseErrorCount: 0,
749
+ unknownEventCount: 0,
750
+ bytesReceived: 0,
751
+ eventTypeCounts: {},
752
+ };
753
+ }
599
754
  function providerEventSummaryForPiEvent(event) {
600
755
  if (!event || typeof event !== "object")
601
756
  return undefined;
@@ -636,6 +791,7 @@ function providerEventSummaryForPiEvent(event) {
636
791
  const byteSize = safeJsonByteSize(safeFields);
637
792
  return {
638
793
  eventType,
794
+ messageEnded: candidate.type === "message_end",
639
795
  assistantEventType: assistantType,
640
796
  parseStatus,
641
797
  normalizedType: normalizedTypeForPiAssistantEvent(assistantType),
@@ -717,8 +873,12 @@ function toolStatusForPiAssistantEvent(type, argsBytes, existingStatus) {
717
873
  return argsBytes > 0 ? "args_partial" : existingStatus ?? "args_started";
718
874
  return existingStatus ?? "args_started";
719
875
  }
720
- function isTerminalToolCallStatus(status) {
721
- return status === "ok" || status === "error" || status === "aborted" || status === "timeout";
876
+ function terminalTurnStatusForSessionError(event) {
877
+ if (event.errorDetails?.category === "runtime_abort" || event.errorDetails?.errorClass === "runtime_abort")
878
+ return "aborted";
879
+ if (event.errorDetails?.code === "timeout")
880
+ return "timeout";
881
+ return "error";
722
882
  }
723
883
  function terminalToolCallStatus(status) {
724
884
  if (status === "aborted")
@@ -14,7 +14,7 @@ import { getMcpAgentContextFile } from "../mcp/agent-context.js";
14
14
  import { createPiboSystemPromptTemplateExtension } from "./system-prompt-template.js";
15
15
  import { getActivePiboBasePromptPath } from "./base-prompt.js";
16
16
  import { createPiboCompactionPromptExtension } from "./compaction-prompt.js";
17
- import { createPiboAssistantContextGuardExtension } from "./context-guard.js";
17
+ import { cancelPiboAssistantContextGuardRecovery, createPiboAssistantContextGuardExtension, createPiboAssistantContextGuardRecovery, isPiboAssistantContextGuardRecoveryPending, registerPiboAssistantContextGuardRecovery, } from "./context-guard.js";
18
18
  import { getPiPackageRuntimeOptions } from "../pi-packages/runtime.js";
19
19
  import { getDefaultPiboWorkspace } from "./workspace.js";
20
20
  import { DEFAULT_USER_TIMEZONE } from "./user-settings.js";
@@ -166,10 +166,10 @@ function getBuiltinToolAllowlist(profile, customTools) {
166
166
  return undefined;
167
167
  return [...selectedBuiltinTools, ...customTools.map((tool) => tool.name)];
168
168
  }
169
- function getProfileExtensionFactories(profile, extensionFactories) {
169
+ function getProfileExtensionFactories(profile, extensionFactories, contextGuardRecovery) {
170
170
  const piboPromptTemplateExtension = createPiboSystemPromptTemplateExtension();
171
171
  const piboCompactionPromptExtension = createPiboCompactionPromptExtension();
172
- const piboContextGuardExtension = createPiboAssistantContextGuardExtension();
172
+ const piboContextGuardExtension = createPiboAssistantContextGuardExtension({}, contextGuardRecovery);
173
173
  const providerToolExtensions = profile.tools
174
174
  .filter((tool) => tool.enabled !== false)
175
175
  .filter(isWebSearchProviderTool)
@@ -234,6 +234,7 @@ export async function createPiboRuntime(options = {}) {
234
234
  const sessionManager = await createSessionManager(cwd, profile, options.persistSession !== false);
235
235
  const authStorage = AuthStorage.create();
236
236
  const createRuntime = async ({ cwd: runtimeCwd, agentDir: runtimeAgentDir, sessionManager: runtimeSessionManager, sessionStartEvent, }) => {
237
+ const contextGuardRecovery = createPiboAssistantContextGuardRecovery();
237
238
  const contextFiles = await loadContextFiles(runtimeCwd, profile.contextFiles);
238
239
  const sessionContextFile = createSessionContextFile({ piboSessionId: profile.sessionId, ...options.sessionContext });
239
240
  const installedToolContextFile = getInstalledCliToolContextFile();
@@ -247,7 +248,7 @@ export async function createPiboRuntime(options = {}) {
247
248
  resourceLoaderOptions: {
248
249
  ...piPackageOptions.resourceLoaderOptions,
249
250
  additionalSkillPaths: skillPaths,
250
- extensionFactories: getProfileExtensionFactories(profile, options.extensionFactories),
251
+ extensionFactories: getProfileExtensionFactories(profile, options.extensionFactories, contextGuardRecovery),
251
252
  noExtensions: true,
252
253
  noSkills: true,
253
254
  noPromptTemplates: true,
@@ -293,6 +294,10 @@ export async function createPiboRuntime(options = {}) {
293
294
  tools: getBuiltinToolAllowlist(profile, customTools),
294
295
  });
295
296
  installValidationOutputCompaction(created.session.agent);
297
+ registerPiboAssistantContextGuardRecovery(created.session, contextGuardRecovery);
298
+ if (options.contextGuardTuiQueueOrdering === true) {
299
+ installPiboContextGuardTuiQueueOrdering(created.session);
300
+ }
296
301
  const resourceLoader = services.resourceLoader;
297
302
  const diagnostics = [
298
303
  ...piPackageOptions.diagnostics,
@@ -303,13 +308,14 @@ export async function createPiboRuntime(options = {}) {
303
308
  message: `Failed to load extension "${path}": ${error}`,
304
309
  })),
305
310
  ];
306
- if (localRuntimeRegistry) {
307
- const originalDispose = created.session.dispose.bind(created.session);
308
- created.session.dispose = () => {
311
+ const originalDispose = created.session.dispose.bind(created.session);
312
+ created.session.dispose = () => {
313
+ cancelPiboAssistantContextGuardRecovery(created.session, new Error("Context guard recovery cancelled because the Pi session was disposed"));
314
+ if (localRuntimeRegistry) {
309
315
  void localRuntimeRegistry.closeControllerSessions(profile.sessionId ?? "local", { force: true });
310
- originalDispose();
311
- };
312
- }
316
+ }
317
+ originalDispose();
318
+ };
313
319
  return {
314
320
  ...created,
315
321
  services,
@@ -456,6 +462,39 @@ export async function inspectPiboProfile(options = {}) {
456
462
  await runtime.dispose();
457
463
  }
458
464
  }
465
+ function installPiboContextGuardTuiQueueOrdering(session) {
466
+ const originalSubscribe = session.subscribe.bind(session);
467
+ const originalPrompt = session.prompt.bind(session);
468
+ const originalSteer = session.steer.bind(session);
469
+ session.subscribe = ((listener) => originalSubscribe((event) => {
470
+ if (event.type === "compaction_end"
471
+ && event.result
472
+ && isPiboAssistantContextGuardRecoveryPending(session)) {
473
+ listener({ ...event, willRetry: true });
474
+ return;
475
+ }
476
+ listener(event);
477
+ }));
478
+ session.prompt = async (text, options) => {
479
+ if (isPiboAssistantContextGuardRecoveryPending(session)) {
480
+ if (!session.isStreaming) {
481
+ await session.followUp(text, options?.images);
482
+ options?.preflightResult?.(true);
483
+ return;
484
+ }
485
+ await originalPrompt(text, { ...options, streamingBehavior: "followUp" });
486
+ return;
487
+ }
488
+ await originalPrompt(text, options);
489
+ };
490
+ session.steer = async (text, images) => {
491
+ if (isPiboAssistantContextGuardRecoveryPending(session)) {
492
+ await session.followUp(text, images);
493
+ return;
494
+ }
495
+ await originalSteer(text, images);
496
+ };
497
+ }
459
498
  export async function runPiboTui(options = {}) {
460
499
  const profile = options.profile ?? createDefaultPiboProfile();
461
500
  const hasEnabledSubagents = profile.subagents.some((subagent) => subagent.enabled !== false);
@@ -465,7 +504,7 @@ export async function runPiboTui(options = {}) {
465
504
  process.exitCode = 1;
466
505
  return;
467
506
  }
468
- const runtime = await createPiboRuntime({ ...options, profile });
507
+ const runtime = await createPiboRuntime({ ...options, profile, contextGuardTuiQueueOrdering: true });
469
508
  try {
470
509
  const fatal = runtime.diagnostics.find((diagnostic) => diagnostic.type === "error");
471
510
  for (const diagnostic of runtime.diagnostics) {