@pasko70/pibo 1.12.0 → 1.13.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.
Files changed (59) hide show
  1. package/dist/apps/chat/chat-trace-helpers.js +59 -2
  2. package/dist/apps/chat/data/timeline-query-service.js +1 -1
  3. package/dist/apps/chat/loop-api.js +5 -0
  4. package/dist/apps/chat/web-app.js +16 -8
  5. package/dist/apps/chat-ui/assets/{dist-D1U9ck8z.js → dist-B_8EcoB6.js} +1 -1
  6. package/dist/apps/{context-files-ui/assets/dist-CT-91DFe.js → chat-ui/assets/dist-CELRUR8b.js} +1 -1
  7. package/dist/apps/{context-files-ui/assets/dist-Bdudyq9I.js → chat-ui/assets/dist-CVLKu7NV.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-DYdgxFZX.js → dist-CVmpu9FK.js} +1 -1
  9. package/dist/apps/{context-files-ui/assets/dist-f2E1-leP.js → chat-ui/assets/dist-CnX234jg.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-D4qbF8wy.js → dist-DVPXynJE.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-B2Pozq2c.js → dist-DugRsLaG.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-DxJWicji.js → dist-KOXty1JB.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-Bgv5Fm3v.js → dist-UYBI2F8E.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-la7ZfDGk.js → dist-XmG4qG-A.js} +1 -1
  15. package/dist/apps/chat-ui/assets/index-DhFQ1th-.js +237 -0
  16. package/dist/apps/chat-ui/assets/index-hjygk9Ol.css +1 -0
  17. package/dist/apps/chat-ui/index.html +2 -2
  18. package/dist/apps/chat-vscode-web/assets/index-1wWonouX.js +41 -0
  19. package/dist/apps/chat-vscode-web/index.html +1 -1
  20. package/dist/apps/{chat-ui/assets/dist-Ci4lft6y.js → context-files-ui/assets/dist-B5mpygOZ.js} +1 -1
  21. package/dist/apps/context-files-ui/assets/{dist-BYcsFgKp.js → dist-BFRDpzcv.js} +1 -1
  22. package/dist/apps/context-files-ui/assets/{dist-DcsDivye.js → dist-BLI6sQZG.js} +1 -1
  23. package/dist/apps/{chat-ui/assets/dist-CkLit_da.js → context-files-ui/assets/dist-B_zXPNMb.js} +1 -1
  24. package/dist/apps/context-files-ui/assets/{dist-uFphVGi8.js → dist-CLrlJjPP.js} +1 -1
  25. package/dist/apps/context-files-ui/assets/{dist-BvIW3V94.js → dist-D6pYe6d_.js} +1 -1
  26. package/dist/apps/{chat-ui/assets/dist-DiPvo4rq.js → context-files-ui/assets/dist-D8ppb7gk.js} +1 -1
  27. package/dist/apps/context-files-ui/assets/{dist-sus90AFR.js → dist-DM6yvMZv.js} +1 -1
  28. package/dist/apps/context-files-ui/assets/{dist-DeozI4lc.js → dist-Em3LWmNi.js} +1 -1
  29. package/dist/apps/context-files-ui/assets/{dist-CSLUWBzz.js → dist-SEP88Pg7.js} +1 -1
  30. package/dist/apps/context-files-ui/assets/{index-CQ25ablG.js → index-C7X2UvWe.js} +3 -3
  31. package/dist/apps/context-files-ui/assets/{index-CukPnd6A.css → index-Cs4XQITe.css} +1 -1
  32. package/dist/apps/context-files-ui/index.html +2 -2
  33. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  34. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.13.0.vsix +0 -0
  35. package/dist/core/gateway-resource-guard.js +46 -15
  36. package/dist/core/routed-session.js +40 -1
  37. package/dist/core/runtime.js +2 -0
  38. package/dist/core/session-errors.js +3 -0
  39. package/dist/core/session-router.js +25 -2
  40. package/dist/core/transcript-integrity.js +431 -0
  41. package/dist/data/pibo-store.js +2 -0
  42. package/dist/data/telemetry.js +148 -0
  43. package/dist/debug/index.js +11 -0
  44. package/dist/gateway/server.js +1 -0
  45. package/dist/reliability/store.js +11 -6
  46. package/dist/runs/registry.js +38 -1
  47. package/dist/runs/resource-isolation.js +437 -0
  48. package/dist/runs/tools.js +6 -3
  49. package/dist/sessions/pibo-data-store.js +114 -0
  50. package/dist/shared/trace-engine.js +3 -3
  51. package/dist/shared/trace-event-projection.js +165 -36
  52. package/dist/shared/trace-nodes.js +6 -4
  53. package/dist/shared/trace-page-merge.js +106 -2
  54. package/dist/shared/trace-transcript.js +194 -36
  55. package/package.json +1 -1
  56. package/dist/apps/chat-ui/assets/index-BJoo_GJP.css +0 -1
  57. package/dist/apps/chat-ui/assets/index-CYxPvrxL.js +0 -237
  58. package/dist/apps/chat-vscode-web/assets/index-CFSHKXsQ.js +0 -41
  59. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.12.0.vsix +0 -0
@@ -1,3 +1,4 @@
1
+ import { ChatDataIngestService } from "../data/ingest-service.js";
1
2
  import { PiboDataStore } from "../data/pibo-store.js";
2
3
  import { createPiboSession, matchesFindInput, } from "./store.js";
3
4
  export class PiboDataSessionStore {
@@ -119,6 +120,70 @@ export class PiboDataSessionStore {
119
120
  getTelemetryStore() {
120
121
  return this.dataStore.telemetry;
121
122
  }
123
+ recoverInterruptedRuntimeState(input = {}) {
124
+ const at = input.at ?? new Date().toISOString();
125
+ const runsBySession = groupRunsByController(input.recoveredRuns ?? []);
126
+ return this.dataStore.transaction(() => {
127
+ const recoveredTurns = this.dataStore.telemetry.recoverInterruptedTurns({
128
+ at,
129
+ resolveOutcome: (turn) => recoveryOutcomeForTurn(turn, runsBySession.get(turn.piboSessionId) ?? []),
130
+ });
131
+ if (recoveredTurns.length === 0)
132
+ return [];
133
+ const ingest = new ChatDataIngestService(this.dataStore);
134
+ const results = [];
135
+ for (const recovered of recoveredTurns) {
136
+ const session = this.get(recovered.turn.piboSessionId);
137
+ if (!session)
138
+ continue;
139
+ const row = this.db.prepare("SELECT room_id FROM sessions WHERE id = ?").get(session.id);
140
+ const event = {
141
+ type: "session_error",
142
+ piboSessionId: session.id,
143
+ eventId: recoveryEventId(recovered.turn),
144
+ error: recovered.outcome.summary,
145
+ errorDetails: {
146
+ category: "runtime_restart",
147
+ errorClass: recovered.outcome.status === "aborted" ? "runtime_abort" : "runtime_error",
148
+ code: recovered.outcome.status === "timeout" ? "timeout" : "runtime_interrupted",
149
+ origin: "runtime",
150
+ severity: "error",
151
+ retryable: false,
152
+ userMessage: "The previous gateway runtime ended before this turn completed.",
153
+ },
154
+ };
155
+ this.db.prepare(`
156
+ UPDATE sessions SET
157
+ status = 'error',
158
+ updated_at = ?,
159
+ last_activity_at = MAX(last_activity_at, ?)
160
+ WHERE id = ? AND deleted_at IS NULL
161
+ `).run(at, at, session.id);
162
+ this.db.prepare(`
163
+ UPDATE session_navigation SET
164
+ status = 'error',
165
+ last_activity_at = MAX(last_activity_at, ?),
166
+ sort_key = MAX(sort_key, ?),
167
+ updated_at = ?
168
+ WHERE session_id = ?
169
+ `).run(at, at, at, session.id);
170
+ ingest.ingestOutputEvent({
171
+ session,
172
+ roomId: row?.room_id ?? undefined,
173
+ actorId: session.id,
174
+ event,
175
+ createdAt: at,
176
+ });
177
+ results.push({
178
+ turnId: recovered.turn.turnId,
179
+ piboSessionId: session.id,
180
+ event,
181
+ outcome: recovered.outcome,
182
+ });
183
+ }
184
+ return results;
185
+ });
186
+ }
122
187
  close() {
123
188
  if (this.ownsDataStore)
124
189
  this.dataStore.close();
@@ -137,6 +202,55 @@ export class PiboDataSessionStore {
137
202
  export function createDefaultPiboDataSessionStore() {
138
203
  return new PiboDataSessionStore(new PiboDataStore());
139
204
  }
205
+ function groupRunsByController(runs) {
206
+ const grouped = new Map();
207
+ for (const run of runs) {
208
+ const items = grouped.get(run.controllerPiboSessionId) ?? [];
209
+ items.push(run);
210
+ grouped.set(run.controllerPiboSessionId, items);
211
+ }
212
+ return grouped;
213
+ }
214
+ function recoveryOutcomeForTurn(turn, runs) {
215
+ if (turn.status === "queued") {
216
+ return {
217
+ status: "aborted",
218
+ summary: "Queued turn was interrupted by gateway restart before it could start.",
219
+ };
220
+ }
221
+ const timedOut = runs.find((run) => run.status === "timed_out");
222
+ if (timedOut) {
223
+ return {
224
+ status: "timeout",
225
+ summary: `Gateway restart recovery timed out yielded run ${timedOut.runId} before this turn completed.`,
226
+ };
227
+ }
228
+ const failed = runs.find((run) => run.status === "failed");
229
+ if (failed) {
230
+ return {
231
+ status: "error",
232
+ summary: `Gateway restart recovery failed yielded run ${failed.runId} before this turn completed.`,
233
+ };
234
+ }
235
+ const queued = runs.find((run) => run.status === "queued");
236
+ if (queued) {
237
+ return {
238
+ status: "aborted",
239
+ summary: `Gateway restart interrupted this turn; yielded run ${queued.runId} was queued for retry.`,
240
+ };
241
+ }
242
+ return {
243
+ status: "aborted",
244
+ summary: "Turn was interrupted by gateway restart.",
245
+ };
246
+ }
247
+ function recoveryEventId(turn) {
248
+ if (turn.eventId)
249
+ return turn.eventId;
250
+ if (turn.inputEventId)
251
+ return turn.inputEventId;
252
+ return turn.turnId.startsWith("turn_") ? turn.turnId.slice("turn_".length) : turn.turnId;
253
+ }
140
254
  function sessionFromRow(row) {
141
255
  return {
142
256
  id: row.id,
@@ -1,5 +1,5 @@
1
1
  import { reconcileAsyncAgentRunStatuses } from "./trace-async-agent-runs.js";
2
- import { applySingleEventToNodes, contentDeltaPatchNodeId, dedupeTraceEvents, eventsCanAffectAsyncAgentRunStatus, findOpenTranscriptEventIds, isConfirmedUserMessageEcho, latestTraceStreamId, mergeMessageTurnTimings, messageTurnTimingsFromEvents, reconcileTranscriptUserMessageTimestamps, traceEventDedupeKey, } from "./trace-event-projection.js";
2
+ import { applySingleEventToNodes, contentDeltaPatchNodeId, dedupeTraceEvents, eventsCanAffectAsyncAgentRunStatus, findOpenTranscriptEventIds, isConfirmedUserMessageEcho, latestTraceStreamId, mergeMessageTurnTimings, messageTurnTimingsFromEvents, reconcileTranscriptUserMessages, traceEventDedupeKey, } from "./trace-event-projection.js";
3
3
  import { flattenTraceNodes, mapTraceNodesById, nestTraceNodes } from "./trace-nodes.js";
4
4
  import { nestMutableCopiedTraceNodes, shareUnchangedTraceNodes } from "./trace-patch-nodes.js";
5
5
  import { mapTraceChildSessionsByParent, mapTraceSubagentSessionLinks, } from "./trace-subagent-links.js";
@@ -13,10 +13,10 @@ export function buildTraceViewFromEvents(input) {
13
13
  const events = dedupeTraceEvents(input.events);
14
14
  const allEntries = input.transcriptEntries ?? [];
15
15
  const openTranscriptEventIds = findOpenTranscriptEventIds(events, sessionStatus);
16
- const entries = projectTranscriptEntries(allEntries, sessionStatus, openTranscriptEventIds);
17
16
  const turnTimings = mergeMessageTurnTimings(input.turnTimings ?? [], messageTurnTimingsFromEvents(events));
17
+ const entries = projectTranscriptEntries(allEntries, sessionStatus, openTranscriptEventIds, turnTimings);
18
18
  const nodes = traceNodesFromEntries(input.session.id, entries, turnTimings);
19
- reconcileTranscriptUserMessageTimestamps(nodes, events);
19
+ reconcileTranscriptUserMessages(nodes, events, turnTimings);
20
20
  const byId = mapTraceNodesById(nodes);
21
21
  const childByParent = mapTraceChildSessionsByParent(input.sessions ?? []);
22
22
  const linkedChildByToolCallId = mapTraceSubagentSessionLinks(events);
@@ -30,7 +30,7 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
30
30
  }
31
31
  if (payload.type === "assistant_message") {
32
32
  const node = assistantMessageNodeFromEvent(piboSessionId, payload, storedEvent.createdAt, storedEvent.eventSequence, storedEvent.streamId, storedEvent.streamFrameIndex);
33
- const existing = byId.get(node.id);
33
+ const existing = byId.get(node.id) ?? findMatchingContentNode(byId, node);
34
34
  if (existing) {
35
35
  mergeAssistantMessageEvent(existing, node);
36
36
  return;
@@ -59,7 +59,7 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
59
59
  }
60
60
  }
61
61
  if (node.type === "assistant.message") {
62
- const existing = byId.get(node.id);
62
+ const existing = byId.get(node.id) ?? findMatchingContentNode(byId, node);
63
63
  if (existing) {
64
64
  mergeAssistantMessageEvent(existing, node);
65
65
  return;
@@ -73,7 +73,7 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
73
73
  }
74
74
  }
75
75
  if (node.type === "model.reasoning") {
76
- const existing = byId.get(node.id);
76
+ const existing = byId.get(node.id) ?? findMatchingContentNode(byId, node);
77
77
  if (existing) {
78
78
  mergeReasoningEvent(existing, node);
79
79
  return;
@@ -194,26 +194,77 @@ export function contentDeltaPatchNodeId(event) {
194
194
  }
195
195
  return undefined;
196
196
  }
197
- export function reconcileTranscriptUserMessageTimestamps(nodes, events) {
197
+ export function reconcileTranscriptUserMessages(nodes, events, turnTimings = []) {
198
198
  const transcriptUsers = nodes.filter((node) => node.type === "user.message" && node.source === "transcript");
199
+ const timingByEventId = new Map(turnTimings.map((timing) => [timing.eventId, timing]));
200
+ let latestTranscriptUser;
201
+ for (const node of nodes) {
202
+ if (node.type === "user.message" && node.source === "transcript") {
203
+ latestTranscriptUser = node;
204
+ continue;
205
+ }
206
+ if (node.type !== "assistant.message" ||
207
+ node.source !== "transcript" ||
208
+ !node.eventId ||
209
+ !latestTranscriptUser ||
210
+ transcriptUserMessageHasCanonicalIdentity(latestTranscriptUser))
211
+ continue;
212
+ const timing = timingByEventId.get(node.eventId);
213
+ if (timing?.userText &&
214
+ normalizedUserMessageText(traceNodeText(latestTranscriptUser)) !== normalizedUserMessageText(timing.userText))
215
+ continue;
216
+ assignTranscriptUserMessageIdentity(latestTranscriptUser, timing);
217
+ }
218
+ let timingCursor = transcriptUsers.length - 1;
219
+ for (let timingIndex = turnTimings.length - 1; timingIndex >= 0; timingIndex -= 1) {
220
+ const timing = turnTimings[timingIndex];
221
+ const prompt = normalizedUserMessageText(timing.userText);
222
+ if (!prompt)
223
+ continue;
224
+ const exactMatchIndex = transcriptUsers.findIndex((node, index) => index <= timingCursor &&
225
+ !transcriptUserMessageHasCanonicalIdentity(node) &&
226
+ node.entryId === timing.eventId);
227
+ let matchIndex = exactMatchIndex;
228
+ if (matchIndex === -1 && (timing.userMessageType === "message_steered" || timing.completedAt)) {
229
+ for (let userIndex = timingCursor; userIndex >= 0; userIndex -= 1) {
230
+ const node = transcriptUsers[userIndex];
231
+ if (transcriptUserMessageHasCanonicalIdentity(node))
232
+ continue;
233
+ if (normalizedUserMessageText(traceNodeText(node)) !== prompt)
234
+ continue;
235
+ matchIndex = userIndex;
236
+ break;
237
+ }
238
+ }
239
+ if (matchIndex === -1)
240
+ continue;
241
+ const matchedNode = transcriptUsers[matchIndex];
242
+ assignTranscriptUserMessageIdentity(matchedNode, timing);
243
+ timingCursor = matchIndex - 1;
244
+ }
199
245
  let userCursor = 0;
200
246
  for (const storedEvent of events) {
201
247
  const event = storedEvent.payload;
202
248
  if ((event.type !== "message_queued" && event.type !== "message_steered") || event.source !== "user")
203
249
  continue;
204
- const eventId = typeof event.eventId === "string" ? event.eventId : storedEvent.eventId;
250
+ const payloadEventId = typeof event.eventId === "string" ? event.eventId : undefined;
251
+ const eventId = payloadEventId ?? storedEvent.eventId;
252
+ const canonicalId = `event:${event.type}:${payloadEventId ?? cryptoSafeId(event)}`;
253
+ const stableKey = eventStableKey(event);
205
254
  const text = typeof event.text === "string" ? event.text : undefined;
206
- const matchIndex = transcriptUsers.findIndex((node, index) => {
207
- if (index < userCursor)
208
- return false;
209
- if (eventId && (node.entryId === eventId || node.stableKey === `entry:${eventId}`))
210
- return true;
211
- return Boolean(text && traceNodeText(node) === text);
212
- });
255
+ const identityMatchIndex = transcriptUsers.findIndex((node) => transcriptUserMessageMatchesIdentity(node, canonicalId, stableKey, eventId));
256
+ const matchIndex = identityMatchIndex !== -1
257
+ ? identityMatchIndex
258
+ : transcriptUsers.findIndex((node, index) => index >= userCursor &&
259
+ !transcriptUserMessageHasCanonicalIdentity(node) &&
260
+ Boolean(text && traceNodeText(node) === text));
213
261
  if (matchIndex === -1)
214
262
  continue;
215
- transcriptUsers[matchIndex].startedAt = storedEvent.createdAt;
216
- userCursor = matchIndex + 1;
263
+ const matchedNode = transcriptUsers[matchIndex];
264
+ matchedNode.id = canonicalId;
265
+ matchedNode.stableKey = stableKey;
266
+ matchedNode.startedAt = storedEvent.createdAt;
267
+ userCursor = Math.max(userCursor, matchIndex + 1);
217
268
  }
218
269
  }
219
270
  export function isConfirmedUserMessageEcho(nodes, event) {
@@ -223,15 +274,46 @@ function confirmedUserMessageEchoNode(nodes, event) {
223
274
  const payload = event.payload;
224
275
  if ((payload.type !== "message_queued" && payload.type !== "message_steered") || payload.source !== "user")
225
276
  return undefined;
226
- const eventId = typeof payload.eventId === "string" ? payload.eventId : event.eventId;
277
+ const payloadEventId = typeof payload.eventId === "string" ? payload.eventId : undefined;
278
+ const eventId = payloadEventId ?? event.eventId;
279
+ const canonicalId = `event:${payload.type}:${payloadEventId ?? cryptoSafeId(payload)}`;
280
+ const stableKey = eventStableKey(payload);
227
281
  const text = typeof payload.text === "string" ? payload.text : undefined;
228
- return flattenTraceNodes([...nodes]).find((node) => {
229
- if (node.type !== "user.message" || node.source !== "transcript")
230
- return false;
231
- if (eventId && (node.entryId === eventId || node.stableKey === `entry:${eventId}`))
232
- return true;
233
- return Boolean(text && traceNodeText(node) === text);
234
- });
282
+ const transcriptUsers = flattenTraceNodes([...nodes]).filter((node) => node.type === "user.message" && node.source === "transcript");
283
+ const identityMatch = transcriptUsers.find((node) => transcriptUserMessageMatchesIdentity(node, canonicalId, stableKey, eventId));
284
+ if (identityMatch)
285
+ return identityMatch;
286
+ return transcriptUsers.find((node) => !transcriptUserMessageHasCanonicalIdentity(node) && Boolean(text && traceNodeText(node) === text));
287
+ }
288
+ function transcriptUserMessageMatchesIdentity(node, canonicalId, stableKey, eventId) {
289
+ if (node.id === canonicalId || node.stableKey === stableKey)
290
+ return true;
291
+ if (!eventId)
292
+ return false;
293
+ if (node.entryId === eventId || node.stableKey === `entry:${eventId}`)
294
+ return true;
295
+ return [node.id, node.stableKey].some((value) => canonicalUserMessageEventId(value) === eventId);
296
+ }
297
+ function transcriptUserMessageHasCanonicalIdentity(node) {
298
+ return [node.id, node.stableKey].some((value) => canonicalUserMessageEventId(value) !== undefined);
299
+ }
300
+ function canonicalUserMessageEventId(value) {
301
+ for (const prefix of ["event:message_queued:", "event:message_steered:"]) {
302
+ if (value?.startsWith(prefix))
303
+ return value.slice(prefix.length);
304
+ }
305
+ return undefined;
306
+ }
307
+ function assignTranscriptUserMessageIdentity(node, timing) {
308
+ if (!timing)
309
+ return;
310
+ const userMessageType = timing.userMessageType ?? "message_queued";
311
+ node.id = `event:${userMessageType}:${timing.eventId}`;
312
+ node.stableKey = `event:${userMessageType}:${timing.eventId}`;
313
+ }
314
+ function normalizedUserMessageText(value) {
315
+ const normalized = value?.replace(/\s+/g, " ").trim();
316
+ return normalized || undefined;
235
317
  }
236
318
  function traceNodeText(node) {
237
319
  if (typeof node.output === "string")
@@ -505,6 +587,11 @@ function mergeAssistantDeltaEvent(nodes, byId, event, sessionStatus, createdAt,
505
587
  nodes.push(node);
506
588
  byId.set(node.id, node);
507
589
  }
590
+ function findMatchingContentNode(byId, update) {
591
+ if (!update.stableKey)
592
+ return undefined;
593
+ return [...byId.values()].find((candidate) => candidate.type === update.type && candidate.stableKey === update.stableKey);
594
+ }
508
595
  function mergeAssistantMessageEvent(target, update) {
509
596
  target.status = update.status;
510
597
  target.summary = update.summary ?? target.summary;
@@ -517,7 +604,8 @@ function mergeThinkingDeltaEvent(nodes, byId, event, sessionStatus, createdAt, e
517
604
  return;
518
605
  const thinkingId = thinkingEventNodeId(event);
519
606
  const id = thinkingId ? thinkingNodeId(thinkingId) : `event:thinking_delta:${cryptoSafeId(event)}`;
520
- const existing = byId.get(id);
607
+ const stableKey = thinkingId ? `reasoning:${thinkingId}` : id;
608
+ const existing = byId.get(id) ?? [...byId.values()].find((candidate) => candidate.type === "model.reasoning" && candidate.stableKey === stableKey);
521
609
  if (existing) {
522
610
  const text = `${typeof existing.output === "string" ? existing.output : ""}${event.text}`;
523
611
  existing.status = sessionStatus === "running" ? "running" : "done";
@@ -537,7 +625,7 @@ function mergeThinkingDeltaEvent(nodes, byId, event, sessionStatus, createdAt, e
537
625
  summary: event.text,
538
626
  output: event.text,
539
627
  source: "event-log",
540
- stableKey: thinkingId ? `reasoning:${thinkingId}` : id,
628
+ stableKey,
541
629
  orderKey: eventTraceNodeOrder(eventSequence, event.type, streamId, streamFrameIndex),
542
630
  children: [],
543
631
  };
@@ -583,12 +671,17 @@ export function mergeMessageTurnTimings(...groups) {
583
671
  byEventId.set(timing.eventId, timing);
584
672
  continue;
585
673
  }
674
+ const reasoningIndices = mergeUniqueIndices(existing.reasoningIndices, timing.reasoningIndices);
675
+ const assistantIndices = mergeUniqueIndices(existing.assistantIndices, timing.assistantIndices);
586
676
  const merged = {
587
677
  eventId: timing.eventId,
588
678
  userText: timing.userText ?? existing.userText,
679
+ userMessageType: timing.userMessageType ?? existing.userMessageType,
589
680
  startedAt: timing.startedAt ?? existing.startedAt,
590
681
  completedAt: timing.completedAt ?? existing.completedAt,
591
682
  durationMs: timing.durationMs ?? existing.durationMs,
683
+ ...(reasoningIndices ? { reasoningIndices } : {}),
684
+ ...(assistantIndices ? { assistantIndices } : {}),
592
685
  };
593
686
  if (merged.durationMs === undefined) {
594
687
  const startedAtMs = parseTimestamp(merged.startedAt);
@@ -601,41 +694,74 @@ export function mergeMessageTurnTimings(...groups) {
601
694
  }
602
695
  return [...byEventId.values()];
603
696
  }
697
+ function mergeUniqueIndices(first, second) {
698
+ if (!first?.length && !second?.length)
699
+ return undefined;
700
+ return [...new Set([...(first ?? []), ...(second ?? [])])];
701
+ }
702
+ function appendUniqueIndex(indices, partIndex) {
703
+ if (indices?.includes(partIndex))
704
+ return indices;
705
+ return [...(indices ?? []), partIndex];
706
+ }
604
707
  export function messageTurnTimingsFromEvents(events) {
605
708
  const timings = new Map();
606
- const completedEventIds = [];
607
- const completedEventIdSet = new Set();
709
+ const eventIds = [];
710
+ const seenEventIds = new Set();
608
711
  const ignoredEventIds = new Set();
609
712
  for (const storedEvent of events) {
610
713
  const event = storedEvent.payload;
611
- if (event.type !== "message_started" && event.type !== "message_finished")
714
+ if (event.type !== "message_queued" &&
715
+ event.type !== "message_steered" &&
716
+ event.type !== "message_started" &&
717
+ event.type !== "message_finished" &&
718
+ event.type !== "session_error" &&
719
+ event.type !== "thinking_finished" &&
720
+ event.type !== "assistant_message")
612
721
  continue;
613
722
  const eventId = typeof event.eventId === "string" ? event.eventId : undefined;
614
723
  if (!eventId)
615
724
  continue;
616
- if (event.type === "message_started" && event.source === "service") {
725
+ if ((event.type === "message_queued" || event.type === "message_started") && event.source === "service") {
617
726
  ignoredEventIds.add(eventId);
618
727
  continue;
619
728
  }
620
729
  if (ignoredEventIds.has(eventId))
621
730
  continue;
731
+ if (!seenEventIds.has(eventId)) {
732
+ eventIds.push(eventId);
733
+ seenEventIds.add(eventId);
734
+ }
622
735
  const timing = timings.get(eventId) ?? {};
623
- if (event.type === "message_started") {
736
+ if (event.type === "message_queued") {
737
+ timing.userText ??= event.text;
738
+ }
739
+ else if (event.type === "message_steered") {
740
+ timing.userText ??= event.text;
741
+ timing.userMessageType = "message_steered";
742
+ }
743
+ else if (event.type === "message_started") {
624
744
  timing.userText ??= event.text;
625
745
  timing.startedAt ??= storedEvent.createdAt;
626
746
  }
627
- else {
747
+ else if (event.type === "message_finished" || event.type === "session_error") {
628
748
  timing.completedAt = storedEvent.createdAt;
629
- if (!completedEventIdSet.has(eventId)) {
630
- completedEventIds.push(eventId);
631
- completedEventIdSet.add(eventId);
632
- }
749
+ }
750
+ else if (event.type === "thinking_finished" && hasVisibleText(event.text)) {
751
+ const partIndex = typeof event.thinkingIndex === "number" ? event.thinkingIndex : event.contentIndex;
752
+ if (typeof partIndex === "number")
753
+ timing.reasoningIndices = appendUniqueIndex(timing.reasoningIndices, partIndex);
754
+ }
755
+ else if (event.type === "assistant_message" && hasVisibleText(event.text)) {
756
+ const partIndex = typeof event.assistantIndex === "number" ? event.assistantIndex : event.contentIndex;
757
+ if (typeof partIndex === "number")
758
+ timing.assistantIndices = appendUniqueIndex(timing.assistantIndices, partIndex);
633
759
  }
634
760
  timings.set(eventId, timing);
635
761
  }
636
- return completedEventIds.flatMap((eventId) => {
762
+ return eventIds.flatMap((eventId) => {
637
763
  const timing = timings.get(eventId);
638
- if (!timing?.completedAt)
764
+ if (!timing)
639
765
  return [];
640
766
  const startedAtMs = parseTimestamp(timing.startedAt);
641
767
  const completedAtMs = parseTimestamp(timing.completedAt);
@@ -643,10 +769,13 @@ export function messageTurnTimingsFromEvents(events) {
643
769
  eventId,
644
770
  userText: timing.userText,
645
771
  startedAt: timing.startedAt,
772
+ ...(timing.userMessageType ? { userMessageType: timing.userMessageType } : {}),
646
773
  completedAt: timing.completedAt,
647
774
  durationMs: startedAtMs === undefined || completedAtMs === undefined
648
775
  ? undefined
649
776
  : Math.max(0, completedAtMs - startedAtMs),
777
+ ...(timing.reasoningIndices ? { reasoningIndices: timing.reasoningIndices } : {}),
778
+ ...(timing.assistantIndices ? { assistantIndices: timing.assistantIndices } : {}),
650
779
  }];
651
780
  });
652
781
  }
@@ -19,14 +19,16 @@ function areTraceNodesSorted(nodes) {
19
19
  return true;
20
20
  }
21
21
  export function compareTraceNodes(left, right) {
22
- const bySameTurnPhase = compareSameTurnPhase(left, right);
23
- if (bySameTurnPhase !== 0)
24
- return bySameTurnPhase;
25
- if (left.orderKey?.sourceRank === right.orderKey?.sourceRank) {
22
+ if (left.orderKey && right.orderKey && left.orderKey.sourceRank === right.orderKey.sourceRank) {
26
23
  const bySameSourceOrder = compareTraceOrder(left.orderKey, right.orderKey);
27
24
  if (bySameSourceOrder !== 0)
28
25
  return bySameSourceOrder;
29
26
  }
27
+ else {
28
+ const bySameTurnPhase = compareSameTurnPhase(left, right);
29
+ if (bySameTurnPhase !== 0)
30
+ return bySameTurnPhase;
31
+ }
30
32
  const byStartTime = compareOptionalIsoTime(left.startedAt, right.startedAt);
31
33
  if (byStartTime !== 0)
32
34
  return byStartTime;
@@ -1,4 +1,5 @@
1
1
  import { flattenTraceNodes, nestTraceNodes } from "./trace-nodes.js";
2
+ import { compareTraceOrder } from "./trace-order.js";
2
3
  export function mergeOlderTracePage(current, older) {
3
4
  if (current.piboSessionId !== older.piboSessionId)
4
5
  return current;
@@ -28,8 +29,8 @@ export function mergeRefreshedTracePage(current, refreshed) {
28
29
  return refreshed;
29
30
  return {
30
31
  ...refreshed,
31
- nodes: mergeTraceNodes(current.nodes, refreshed.nodes),
32
- rawEvents: current.rawEvents.length ? current.rawEvents : refreshed.rawEvents,
32
+ nodes: mergeRefreshedTraceNodes(current.nodes, refreshed),
33
+ rawEvents: mergeTraceRawEvents(current.rawEvents, refreshed.rawEvents),
33
34
  beforeCursor: current.beforeCursor,
34
35
  firstEventSequence: current.firstEventSequence ?? refreshed.firstEventSequence,
35
36
  nextBeforeSequence: current.nextBeforeSequence,
@@ -38,6 +39,109 @@ export function mergeRefreshedTracePage(current, refreshed) {
38
39
  eventLimit: current.eventLimit ?? refreshed.eventLimit,
39
40
  };
40
41
  }
42
+ function mergeRefreshedTraceNodes(currentNodes, refreshed) {
43
+ const flattenedCurrentNodes = flattenTraceNodes([...currentNodes]);
44
+ const refreshedNodes = flattenTraceNodes([...refreshed.nodes]);
45
+ const refreshedIds = new Set(refreshedNodes.map((node) => node.id));
46
+ const canonicalTranscriptEventIds = transcriptEventIds([...flattenedCurrentNodes, ...refreshedNodes]);
47
+ const refreshedOrderBoundaries = earliestTraceNodeOrdersBySource(refreshedNodes);
48
+ const refreshedStartedAt = earliestTraceNodeTimestamp(refreshedNodes);
49
+ const retainedOlderNodes = flattenedCurrentNodes.filter((node) => {
50
+ if (refreshedIds.has(node.id))
51
+ return true;
52
+ if (node.type === "agent.turn" &&
53
+ node.source === "event-log" &&
54
+ node.eventId &&
55
+ canonicalTranscriptEventIds.has(node.eventId))
56
+ return false;
57
+ if (isTransientTailNode(node))
58
+ return false;
59
+ const eventSequence = node.orderKey?.eventSequence;
60
+ if (eventSequence !== undefined && refreshed.firstEventSequence !== undefined) {
61
+ return eventSequence < refreshed.firstEventSequence;
62
+ }
63
+ const refreshedOrderBoundary = node.orderKey
64
+ ? refreshedOrderBoundaries.get(node.orderKey.sourceRank)
65
+ : undefined;
66
+ if (node.orderKey && refreshedOrderBoundary) {
67
+ return compareTraceOrder(node.orderKey, refreshedOrderBoundary) < 0;
68
+ }
69
+ if (node.source === "live" || node.orderKey?.streamId !== undefined)
70
+ return false;
71
+ const startedAt = parseTimestamp(node.startedAt);
72
+ return startedAt !== undefined && refreshedStartedAt !== undefined && startedAt < refreshedStartedAt;
73
+ });
74
+ const mergedIds = new Set([...retainedOlderNodes, ...refreshedNodes].map((node) => node.id));
75
+ const removedCurrentIds = new Set(flattenedCurrentNodes
76
+ .filter((node) => !mergedIds.has(node.id))
77
+ .map((node) => node.id));
78
+ return mergeTraceNodes(clearRemovedParentIds(retainedOlderNodes, removedCurrentIds), clearRemovedParentIds(refreshedNodes, removedCurrentIds));
79
+ }
80
+ function clearRemovedParentIds(nodes, removedIds) {
81
+ return nodes.map((node) => node.parentId && removedIds.has(node.parentId)
82
+ ? { ...node, parentId: undefined }
83
+ : node);
84
+ }
85
+ function transcriptEventIds(nodes) {
86
+ const eventIds = new Set();
87
+ for (const node of nodes) {
88
+ if (node.source === "transcript" && node.eventId)
89
+ eventIds.add(node.eventId);
90
+ }
91
+ return eventIds;
92
+ }
93
+ function isTransientTailNode(node) {
94
+ return node.source === "live" || node.orderKey?.streamId !== undefined;
95
+ }
96
+ function earliestTraceNodeOrdersBySource(nodes) {
97
+ const earliestBySource = new Map();
98
+ for (const node of nodes) {
99
+ if (!node.orderKey)
100
+ continue;
101
+ const earliest = earliestBySource.get(node.orderKey.sourceRank);
102
+ if (!earliest || compareTraceOrder(node.orderKey, earliest) < 0) {
103
+ earliestBySource.set(node.orderKey.sourceRank, node.orderKey);
104
+ }
105
+ }
106
+ return earliestBySource;
107
+ }
108
+ function earliestTraceNodeTimestamp(nodes) {
109
+ let earliest;
110
+ for (const node of nodes) {
111
+ const startedAt = parseTimestamp(node.startedAt);
112
+ if (startedAt === undefined)
113
+ continue;
114
+ earliest = earliest === undefined ? startedAt : Math.min(earliest, startedAt);
115
+ }
116
+ return earliest;
117
+ }
118
+ function parseTimestamp(value) {
119
+ if (!value)
120
+ return undefined;
121
+ const parsed = Date.parse(value);
122
+ return Number.isFinite(parsed) ? parsed : undefined;
123
+ }
124
+ function mergeTraceRawEvents(currentEvents, refreshedEvents) {
125
+ if (!refreshedEvents.length)
126
+ return currentEvents;
127
+ const firstRefreshedSequence = refreshedEvents.reduce((first, event) => {
128
+ if (event.eventSequence === undefined)
129
+ return first;
130
+ return first === undefined ? event.eventSequence : Math.min(first, event.eventSequence);
131
+ }, undefined);
132
+ const byKey = new Map();
133
+ for (const event of currentEvents) {
134
+ if (firstRefreshedSequence !== undefined && event.eventSequence !== undefined && event.eventSequence >= firstRefreshedSequence)
135
+ continue;
136
+ byKey.set(traceRawEventKey(event), event);
137
+ }
138
+ for (const event of refreshedEvents)
139
+ byKey.set(traceRawEventKey(event), event);
140
+ return [...byKey.values()];
141
+ }
142
+ function traceRawEventKey(event) {
143
+ return event.id || `${event.eventSequence ?? ""}:${event.type}:${event.createdAt}`;
144
+ }
41
145
  function mergeTraceNodes(olderNodes, currentNodes) {
42
146
  const byId = new Map();
43
147
  for (const node of flattenTraceNodes([...olderNodes])) {