@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.
@@ -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);
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
+ }
120
152
  }
121
- this.startOrProgressPhase(turn, "provider_stream", now, "provider event metadata", { providerRequestId: providerRequest.providerRequestId });
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 });
155
+ }
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,19 +416,19 @@ 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);
419
+ recordTurnTerminal(event, context, status, phaseName, summary, errorCategory) {
420
+ const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, undefined, context, { includeTerminal: true }) ?? this.activeTurnContext(event.piboSessionId, context);
367
421
  if (!turn)
368
422
  return;
369
- this.startOrProgressPhase(turn, phaseName, new Date().toISOString(), summary, { updateTurn: true });
370
- }
371
- recordTurnTerminal(event, context, status, phaseName, summary) {
372
- const turn = this.turnContextForEvent(event.piboSessionId, event.eventId, undefined, context) ?? this.activeTurnContext(event.piboSessionId, context);
373
- if (!turn)
423
+ const existingTurn = this.store?.getTurn(turn.turnId);
424
+ if (existingTurn && TERMINAL_TURN_STATUSES.has(existingTurn.status)) {
425
+ this.clearTurnProgress(turn.turnId);
374
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
- this.finishActiveProviderRequests(turn.turnId, providerStatusForTurnStatus(status), now, summary);
431
+ this.finishActiveProviderRequests(turn.turnId, providerStatusForTurnStatus(status), now, summary, errorCategory);
378
432
  this.finishActiveToolCalls(turn.turnId, status, now, summary);
379
433
  this.telemetry.upsertPhase({
380
434
  phaseId: phaseId(turn.turnId, phaseName),
@@ -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,39 +518,117 @@ 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;
488
612
  }
489
- finishActiveProviderRequests(turnId, status, now, summary) {
490
- const timeline = this.store?.getTurnTimeline(turnId, { limit: 100 });
491
- for (const request of timeline?.providerRequests ?? []) {
492
- if (isTerminalProviderStatus(request.status))
493
- continue;
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
+ }
625
+ }
626
+ finishActiveProviderRequests(turnId, status, now, summary, errorCategory) {
627
+ for (const request of this.store?.listActiveProviderRequestsForTurn(turnId) ?? []) {
494
628
  this.upsertProviderRequestFromExisting(request, {
495
629
  status,
496
630
  completedAt: now,
497
- errorCategory: status === "error" ? "runtime_error" : undefined,
631
+ errorCategory: status === "error" ? errorCategory ?? "runtime_error" : undefined,
498
632
  errorMessage: status === "error" ? safeSummary(summary) : undefined,
499
633
  });
500
634
  }
@@ -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")