@pasko70/pibo 3.4.4 → 3.5.1

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 (91) hide show
  1. package/dist/agent-runtime/routed-session.js +111 -25
  2. package/dist/agent-runtimes/codex-native/turn.js +27 -3
  3. package/dist/agent-runtimes/pi/adapter.js +1 -0
  4. package/dist/agent-runtimes/pi/routed-session.js +2 -2
  5. package/dist/apps/chat/bounded-event-stream.js +98 -0
  6. package/dist/apps/chat/chat-settings-routes.js +3 -3
  7. package/dist/apps/chat/data/chat-data-mappers.js +40 -14
  8. package/dist/apps/chat/data/event-command-service.js +30 -21
  9. package/dist/apps/chat/data/history-query-service.js +34 -25
  10. package/dist/apps/chat/data/read-state-service.js +37 -3
  11. package/dist/apps/chat/data/session-query-service.js +19 -11
  12. package/dist/apps/chat/data/timeline-query-service.js +19 -7
  13. package/dist/apps/chat/message-command-dispatcher.js +149 -0
  14. package/dist/apps/chat/output-compactor.js +9 -0
  15. package/dist/apps/chat/output-event-policy.js +9 -1
  16. package/dist/apps/chat/stream.js +21 -3
  17. package/dist/apps/chat/telemetry-retention-service.js +113 -8
  18. package/dist/apps/chat/trace-response-cache.js +46 -0
  19. package/dist/apps/chat/trace-v2.js +12 -6
  20. package/dist/apps/chat/trace.js +1 -1
  21. package/dist/apps/chat/web-app.js +645 -297
  22. package/dist/apps/chat-ui/assets/{dist-V06sfuZa.js → dist-B-auLrzD.js} +1 -1
  23. package/dist/apps/chat-ui/assets/{dist-CH3SvpYV.js → dist-BA_dsINH.js} +1 -1
  24. package/dist/apps/chat-ui/assets/{dist-23lt_7qm.js → dist-eJZar_0-.js} +1 -1
  25. package/dist/apps/chat-ui/assets/{dist-Bf2KScPo.js → dist-wNNR2Bci.js} +1 -1
  26. package/dist/apps/chat-ui/assets/{dist-DsgL8w-W.js → dist-zcmEsIEp.js} +1 -1
  27. package/dist/apps/chat-ui/assets/index-DEkbN5Vo.js +229 -0
  28. package/dist/apps/chat-ui/assets/index-DZK6Tzil.css +1 -0
  29. package/dist/apps/chat-ui/index.html +2 -2
  30. package/dist/apps/chat-vscode-web/assets/index-DZgW1fCB.js +44 -0
  31. package/dist/apps/chat-vscode-web/index.html +1 -1
  32. package/dist/cli-session/localSessionSource.js +3 -2
  33. package/dist/compute/pool/seeds.js +25 -3
  34. package/dist/core/events.js +4 -0
  35. package/dist/core/output-render-sequence.js +65 -6
  36. package/dist/core/provider-capacity.js +33 -0
  37. package/dist/core/provider-telemetry.js +21 -6
  38. package/dist/core/runtime-capacity.js +174 -0
  39. package/dist/core/runtime-telemetry.js +40 -12
  40. package/dist/core/session-router.js +178 -13
  41. package/dist/data/async-chat-reads.js +38 -0
  42. package/dist/data/async-chat-storage.js +96 -0
  43. package/dist/data/async-telemetry-maintenance.js +9 -0
  44. package/dist/data/bounded-worker-client.js +256 -0
  45. package/dist/data/chat-read-projections.js +159 -0
  46. package/dist/data/chat-read-worker.js +73 -0
  47. package/dist/data/chat-storage-worker.js +167 -0
  48. package/dist/data/ingest-service.js +152 -18
  49. package/dist/data/message-command-store.js +290 -0
  50. package/dist/data/payload-store.js +92 -11
  51. package/dist/data/pibo-store.js +10 -8
  52. package/dist/data/schema.js +39 -4
  53. package/dist/data/session-store.js +3 -1
  54. package/dist/data/storage-backup.js +278 -0
  55. package/dist/data/storage-maintenance.js +344 -0
  56. package/dist/data/storage-verification-worker.js +25 -0
  57. package/dist/data/telemetry-capture.js +188 -0
  58. package/dist/data/telemetry-command.js +3 -0
  59. package/dist/data/telemetry-maintenance-worker.js +40 -0
  60. package/dist/data/telemetry-maintenance.js +110 -0
  61. package/dist/data/telemetry-retention.js +16 -7
  62. package/dist/data/telemetry-worker.js +111 -0
  63. package/dist/data/telemetry-writer.js +150 -83
  64. package/dist/data/telemetry.js +5 -0
  65. package/dist/debug/index.js +207 -1
  66. package/dist/debug/message-queue.js +108 -0
  67. package/dist/debug/output-collision-repair.js +140 -0
  68. package/dist/debug/output-integrity.js +38 -2
  69. package/dist/debug/output-repair.js +1 -0
  70. package/dist/debug/storage-backup.js +41 -0
  71. package/dist/debug/storage-maintenance.js +78 -0
  72. package/dist/debug/telemetry-capture.js +66 -0
  73. package/dist/gateway/cli.js +71 -7
  74. package/dist/gateway/server.js +3 -0
  75. package/dist/providers/openai-gpt56.js +11 -6
  76. package/dist/reliability/store.js +119 -23
  77. package/dist/session-ui/terminalRows.js +54 -13
  78. package/dist/sessions/pibo-data-store.js +19 -14
  79. package/dist/shared/debug-features.js +4 -0
  80. package/dist/shared/model-inference-metrics.js +23 -0
  81. package/dist/shared/trace-event-projection.js +69 -2
  82. package/dist/shared/trace-history.js +9 -0
  83. package/dist/shared/trace-live-reducer.js +1 -0
  84. package/dist/shared/trace-patch-nodes.js +19 -0
  85. package/dist/web/channel.js +117 -12
  86. package/dist/web/http.js +135 -41
  87. package/npm-shrinkwrap.json +2 -2
  88. package/package.json +1 -1
  89. package/dist/apps/chat-ui/assets/index-BOceJ0jM.css +0 -1
  90. package/dist/apps/chat-ui/assets/index-BOemYq-V.js +0 -228
  91. package/dist/apps/chat-vscode-web/assets/index-CMwTB8o8.js +0 -43
@@ -16,14 +16,15 @@ export function buildCompactTerminalRows(traceView, options) {
16
16
  if (!traceView)
17
17
  return [];
18
18
  const turnById = mapTurnNodes(traceView.nodes);
19
+ const showToolDebugMetrics = Boolean(options.debugMode && (options.debugFeatures?.toolMetrics ?? true));
19
20
  const flatNodes = flattenTraceNodes(traceView.nodes)
20
21
  .sort((left, right) => compareTraceNodes(left.node, right.node))
21
22
  .filter((item) => item.node.type !== "agent.turn" && (options.showThinking || item.node.type !== "model.reasoning"));
22
23
  const candidates = syncThinkingToolRows(flatNodes.map((item) => createRowCandidate(item.node, item.turnId)));
23
24
  applyCompletedTurnTiming(candidates, turnById);
24
25
  const reconciled = reconcileConceptualRowCandidates(candidates);
25
- const rows = !options.debugMode && (options.toolDisplayMode ?? "default") === "default"
26
- ? groupRelatedToolCandidates(reconciled).map((candidate) => candidate.row)
26
+ const rows = (options.toolDisplayMode ?? "default") === "default"
27
+ ? groupRelatedToolCandidates(reconciled, showToolDebugMetrics).map((candidate) => candidate.row)
27
28
  : reconciled.map((candidate) => candidate.row);
28
29
  return applyToolDisplayMode(rows, options.toolDisplayMode ?? "default");
29
30
  }
@@ -159,18 +160,34 @@ function createRowCandidate(node, turnId) {
159
160
  };
160
161
  break;
161
162
  }
163
+ const isToolCall = node.type === "tool.call" || node.type === "tool.result" || (node.type === "agent.delegation" && Boolean(node.toolCallId));
164
+ const reference = isToolCall ? toolCallReference(node) : undefined;
162
165
  return {
163
166
  ...candidate,
164
167
  row: {
165
168
  ...candidate.row,
166
169
  id: compactTerminalRowIdentity(node),
167
170
  intent: node.intent,
168
- isToolCall: node.type === "tool.call" || node.type === "tool.result" || (node.type === "agent.delegation" && Boolean(node.toolCallId)),
171
+ isToolCall,
172
+ toolCallReference: reference,
173
+ expandable: reference || Object.values(node.payloadRefs ?? {}).some(Boolean) ? true : candidate.row.expandable,
169
174
  toolMetrics: node.toolMetrics,
175
+ modelInferences: node.modelInferences,
170
176
  ...debugFields(node),
171
177
  },
172
178
  };
173
179
  }
180
+ function toolCallReference(node) {
181
+ if (!node.toolCallId)
182
+ return undefined;
183
+ const parsed = parseTraceToolNodeIdentity(node.id);
184
+ return {
185
+ traceNodeId: node.id,
186
+ toolCallId: node.toolCallId,
187
+ eventId: node.eventId ?? parsed?.qualifier?.eventId,
188
+ invocationOrdinal: node.toolInvocationOrdinal ?? parsed?.qualifier?.invocationOrdinal,
189
+ };
190
+ }
174
191
  export function compactTerminalRowIdentity(node) {
175
192
  if (node.type === "tool.call" || node.type === "tool.result" || node.type === "agent.delegation") {
176
193
  if (node.toolCallId) {
@@ -226,6 +243,7 @@ function reconcileConceptualRowCandidates(candidates) {
226
243
  output: candidate.row.output ?? existing.row.output,
227
244
  error: candidate.row.error ?? existing.row.error,
228
245
  payloadRefs: { ...existing.row.payloadRefs, ...candidate.row.payloadRefs },
246
+ modelInferences: mergeModelInferences(existing.row.modelInferences, candidate.row.modelInferences),
229
247
  imagePreviews: mergeImagePreviews(existing.row.imagePreviews, candidate.row.imagePreviews),
230
248
  },
231
249
  };
@@ -310,6 +328,16 @@ function assistantPartIndex(rowId) {
310
328
  const index = Number(match[1]);
311
329
  return Number.isSafeInteger(index) ? index : undefined;
312
330
  }
331
+ function mergeModelInferences(existing, candidate) {
332
+ if (!existing?.length)
333
+ return candidate;
334
+ if (!candidate?.length)
335
+ return existing;
336
+ const merged = new Map(existing.map((record) => [record.id, record]));
337
+ for (const record of candidate)
338
+ merged.set(record.id, record);
339
+ return [...merged.values()];
340
+ }
313
341
  function mergeImagePreviews(existing, candidate) {
314
342
  if (!existing?.length)
315
343
  return candidate?.slice(0, MAX_COMPACT_TERMINAL_IMAGE_PREVIEWS);
@@ -329,6 +357,9 @@ function applyCompletedTurnTiming(candidates, turnById) {
329
357
  continue;
330
358
  const turnCandidates = candidates.filter((candidate) => candidate.turnId === turn.id);
331
359
  const finalCandidate = turnCandidates.at(-1);
360
+ if (finalCandidate && turn.modelInferences?.length) {
361
+ finalCandidate.row.modelInferences = mergeModelInferences(finalCandidate.row.modelInferences, turn.modelInferences);
362
+ }
332
363
  if (finalCandidate?.row.kind !== "message.assistant" || finalCandidate.row.status === "running")
333
364
  continue;
334
365
  finalCandidate.row.startedAt = turn.startedAt;
@@ -362,7 +393,8 @@ function createUserMessageRow(node) {
362
393
  lines: [{ prefix: "prompt", tokens: [token(text)] }],
363
394
  sourceNodeIds: [node.id],
364
395
  forkEntryId: node.entryId,
365
- pendingMessageDelivery: pendingUserMessageDelivery(node),
396
+ pendingMessageDelivery: pendingUserMessageDelivery(node) ?? (node.status === "running" && node.messageDeliveryState ? "queue" : undefined),
397
+ messageDeliveryState: node.messageDeliveryState,
366
398
  startedAt: node.startedAt,
367
399
  output: text,
368
400
  payloadRefs: node.payloadRefs,
@@ -657,9 +689,16 @@ function createCompactionRow(node) {
657
689
  input: node.input,
658
690
  output: node.output,
659
691
  error: node.error,
660
- expandable: node.input !== undefined || node.output !== undefined || Boolean(node.error),
692
+ compactionStats: node.compactionStats,
693
+ compactionMarkdown: compactionMarkdown(node.output),
694
+ expandable: node.status === "error" && (node.input !== undefined || node.output !== undefined || Boolean(node.error)),
661
695
  };
662
696
  }
697
+ function compactionMarkdown(value) {
698
+ if (!isRecord(value))
699
+ return undefined;
700
+ return stringValue(value.summary);
701
+ }
663
702
  function createExecutionCommandRow(node) {
664
703
  if (node.title === "status") {
665
704
  return createStatusToolRow(node);
@@ -903,12 +942,12 @@ function isThinkingOutput(value) {
903
942
  function isThinkingLevelSetOutput(value) {
904
943
  return isRecord(value) && value.action === "set_thinking_level";
905
944
  }
906
- function groupRelatedToolCandidates(candidates) {
945
+ function groupRelatedToolCandidates(candidates, imagesOnly = false) {
907
946
  const grouped = [];
908
947
  for (let index = 0; index < candidates.length; index += 1) {
909
948
  const candidate = candidates[index];
910
949
  const groupKind = candidateGroupKind(candidate);
911
- if (!groupKind) {
950
+ if (!groupKind || (imagesOnly && groupKind !== "images")) {
912
951
  grouped.push(candidate);
913
952
  continue;
914
953
  }
@@ -966,13 +1005,14 @@ function createExploringGroup(candidates) {
966
1005
  }] : []),
967
1006
  ],
968
1007
  sourceNodeIds: candidates.flatMap((candidate) => candidate.row.sourceNodeIds),
1008
+ modelInferences: candidates.reduce((records, candidate) => mergeModelInferences(records, candidate.row.modelInferences), undefined),
969
1009
  eventId: firstRow?.eventId,
970
1010
  runId: firstRow?.runId,
971
1011
  orderSource: firstRow?.orderSource,
972
1012
  orderStreamId: firstRow?.orderStreamId,
973
1013
  orderStreamFrameIndex: firstRow?.orderStreamFrameIndex,
974
1014
  detailItems,
975
- expandable: detailItems.some((item) => item.input !== undefined || item.output !== undefined || Boolean(item.error)),
1015
+ expandable: detailItems.some((item) => item.toolCallReference || item.input !== undefined || item.output !== undefined || Boolean(item.error)),
976
1016
  previewOmission: omittedDetailCount > 0 ? {
977
1017
  source: "details",
978
1018
  visibleLineCount: visibleDetailItems.length,
@@ -988,21 +1028,20 @@ function createImageGroup(candidates) {
988
1028
  const omittedDetailCount = Math.max(0, detailItems.length - visibleDetailItems.length);
989
1029
  const firstRow = candidates[0]?.row;
990
1030
  const firstId = firstRow?.id ?? "images";
991
- const lastId = candidates[candidates.length - 1]?.row.id ?? firstId;
992
1031
  const status = candidates.some((candidate) => candidate.row.status === "running")
993
1032
  ? "running"
994
1033
  : candidates.some((candidate) => candidate.row.status === "error")
995
1034
  ? "error"
996
1035
  : "done";
997
1036
  return {
998
- id: `group:images:${firstId}:${lastId}`,
1037
+ id: firstId,
999
1038
  kind: "tool.group.images",
1000
1039
  status,
1001
1040
  errorKind: status === "error" ? "tool" : undefined,
1002
1041
  lines: [
1003
1042
  {
1004
1043
  prefix: "bullet",
1005
- tokens: [token(status === "running" ? "Viewing images" : status === "error" ? "Image reads failed" : "Viewed images", toneForStatus(status), "semibold")],
1044
+ tokens: [token(status === "running" ? `Viewing ${detailItems.length} images` : status === "error" ? `${detailItems.length} image reads · error` : `${detailItems.length} ${detailItems.length === 1 ? "Image Viewed" : "Images Viewed"}`, toneForStatus(status), "semibold")],
1006
1045
  },
1007
1046
  ...visibleDetailItems.map((item, index) => ({
1008
1047
  prefix: index === 0 ? "detail" : "continuation",
@@ -1014,14 +1053,15 @@ function createImageGroup(candidates) {
1014
1053
  }] : []),
1015
1054
  ],
1016
1055
  sourceNodeIds: candidates.flatMap((candidate) => candidate.row.sourceNodeIds),
1056
+ modelInferences: candidates.reduce((records, candidate) => mergeModelInferences(records, candidate.row.modelInferences), undefined),
1017
1057
  eventId: firstRow?.eventId,
1018
1058
  runId: firstRow?.runId,
1019
1059
  orderSource: firstRow?.orderSource,
1020
1060
  orderStreamId: firstRow?.orderStreamId,
1021
1061
  orderStreamFrameIndex: firstRow?.orderStreamFrameIndex,
1022
1062
  detailItems,
1023
- expandable: detailItems.some((item) => item.input !== undefined || item.output !== undefined || Boolean(item.error)),
1024
- imagePreviews: detailItems.flatMap((item) => item.imagePreviews ?? []).slice(0, MAX_COMPACT_TERMINAL_IMAGE_PREVIEWS),
1063
+ expandable: detailItems.some((item) => item.toolCallReference || item.input !== undefined || item.output !== undefined || Boolean(item.error)),
1064
+ imagePreviews: detailItems.flatMap((item) => item.imagePreviews ?? []),
1025
1065
  previewOmission: omittedDetailCount > 0 ? {
1026
1066
  source: "details",
1027
1067
  visibleLineCount: visibleDetailItems.length,
@@ -1043,6 +1083,7 @@ function detailItemsForGroup(candidates, kind) {
1043
1083
  error: candidate.row.error,
1044
1084
  payloadRefs: candidate.row.payloadRefs,
1045
1085
  linkedPiboSessionId: candidate.row.linkedPiboSessionId,
1086
+ toolCallReference: candidate.row.toolCallReference,
1046
1087
  previewOmission: candidate.row.previewOmission,
1047
1088
  imagePreviews: candidate.row.imagePreviews,
1048
1089
  };
@@ -49,6 +49,7 @@ export class PiboDataSessionStore {
49
49
  const row = this.db.prepare(`${SESSION_SELECT} WHERE s.id = ? AND s.deleted_at IS NULL`).get(id);
50
50
  return row ? sessionFromRow(row) : undefined;
51
51
  }
52
+ getStructureRevision() { return this.db.prepare("SELECT revision FROM chat_navigation_clock WHERE id=1").get().revision; }
52
53
  list() {
53
54
  return this.db.prepare(`${SESSION_SELECT} WHERE s.deleted_at IS NULL ORDER BY s.updated_at DESC`).all().map(sessionFromRow);
54
55
  }
@@ -226,21 +227,21 @@ export class PiboDataSessionStore {
226
227
  if (typeof index !== "number" || !Number.isSafeInteger(index) || index < 0)
227
228
  continue;
228
229
  maximum = Math.max(maximum, index);
229
- persistedParts.push({ index, attributes });
230
+ persistedParts.push({ index, type: row.type, attributes });
230
231
  }
231
- // An unfinished durable part may still belong to another process. Only a
232
- // completed turn provides enough evidence to reattach an exact replay.
233
- if (turnCompleted) {
234
- const matchesReplay = ({ attributes }) => attributes.outputPartFingerprint === input.fingerprint
235
- || attributes.identityFingerprint === input.identityFingerprint;
236
- const replay = (input.suppliedIndex === undefined
237
- ? undefined
238
- : persistedParts.find((part) => part.index === input.suppliedIndex && matchesReplay(part)))
239
- ?? persistedParts.find(matchesReplay);
240
- if (replay) {
241
- this.observeOutputPartIndex(input, replay.index);
242
- return replay.index;
243
- }
232
+ // A completed turn or an exact terminal-part fingerprint is durable
233
+ // evidence for replay. Nonterminal parts remain unattached while the turn
234
+ // is open because another producer may still own them.
235
+ const matchesReplay = ({ type, attributes }) => (turnCompleted || outputPartEventIsTerminal(type))
236
+ && (attributes.outputPartFingerprint === input.fingerprint
237
+ || attributes.identityFingerprint === input.identityFingerprint);
238
+ const replay = (input.suppliedIndex === undefined
239
+ ? undefined
240
+ : persistedParts.find((part) => part.index === input.suppliedIndex && matchesReplay(part)))
241
+ ?? (turnCompleted ? persistedParts.find(matchesReplay) : undefined);
242
+ if (replay) {
243
+ this.observeOutputPartIndex(input, replay.index);
244
+ return replay.index;
244
245
  }
245
246
  const minimum = Math.max(input.proposedIndex, maximum + 1);
246
247
  const row = this.db.prepare(`
@@ -537,6 +538,7 @@ export class PiboDataSessionStore {
537
538
  actorId: session.id,
538
539
  event,
539
540
  createdAt: at,
541
+ persistenceProvenance: { producer: "runtime-recovery", projection: "product-history", phase: "startup-recovery" },
540
542
  });
541
543
  results.push({
542
544
  turnId: recovered.turn.turnId,
@@ -725,6 +727,9 @@ function outputPartEventTypes(kind) {
725
727
  case "compaction": return ["compaction_start", "compaction_end"];
726
728
  }
727
729
  }
730
+ function outputPartEventIsTerminal(type) {
731
+ return type === "assistant_message" || type === "thinking_finished" || type === "assistant_usage" || type === "compaction_end";
732
+ }
728
733
  function parseJsonObject(json) {
729
734
  if (!json)
730
735
  return {};
@@ -0,0 +1,4 @@
1
+ export const DEFAULT_DEBUG_FEATURE_SETTINGS = {
2
+ toolMetrics: true,
3
+ modelInferenceMetrics: true,
4
+ };
@@ -0,0 +1,23 @@
1
+ export function modelInferenceInputTokens(metrics) {
2
+ const total = tokenCount(metrics?.totalTokens);
3
+ const output = tokenCount(metrics?.outputTokens);
4
+ if (total !== undefined && output !== undefined)
5
+ return Math.max(0, total - output);
6
+ return tokenCount(metrics?.inputTokens);
7
+ }
8
+ export function modelInferenceCachedInputTokens(metrics) {
9
+ const cacheRead = tokenCount(metrics?.cacheReadTokens);
10
+ const cacheWrite = tokenCount(metrics?.cacheWriteTokens);
11
+ if (cacheRead === undefined && cacheWrite === undefined)
12
+ return undefined;
13
+ return (cacheRead ?? 0) + (cacheWrite ?? 0);
14
+ }
15
+ export function modelInferenceUncachedInputTokens(metrics) {
16
+ const input = modelInferenceInputTokens(metrics);
17
+ if (input === undefined)
18
+ return undefined;
19
+ return Math.max(0, input - (modelInferenceCachedInputTokens(metrics) ?? 0));
20
+ }
21
+ function tokenCount(value) {
22
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : undefined;
23
+ }
@@ -6,6 +6,10 @@ import { findLikelyTraceChildSession, isSubagentToolName, } from "./trace-subage
6
6
  import { qualifiedToolNodeId } from "./trace-tool-identity.js";
7
7
  export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent, childByParent, linkedChildByToolCallId, historyCoverage, openTranscriptEventIds, sessionStatus) {
8
8
  const payload = storedEvent.payload;
9
+ if (payload.type === "assistant_usage") {
10
+ attachModelInferenceToLatestOutput(nodes, byId, payload, storedEvent);
11
+ return;
12
+ }
9
13
  if (payload.type === "message_started" && payload.source === "user" && payload.eventId) {
10
14
  const persistedUser = flattenTraceNodes([...nodes]).find((node) => node.type === "user.message"
11
15
  && isPersistedHistorySource(node.source)
@@ -42,6 +46,7 @@ export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent,
42
46
  }
43
47
  if (payload.type === "assistant_message") {
44
48
  const node = assistantMessageNodeFromEvent(piboSessionId, payload, storedEvent.createdAt, storedEvent.eventSequence, storedEvent.streamId, storedEvent.streamFrameIndex, storedEvent.traceSource, storedEvent.id);
49
+ applyStoredPayloadRef(node, payload, storedEvent.storedPayloadRef);
45
50
  const existing = byId.get(node.id) ?? findMatchingContentNode(byId, node);
46
51
  if (existing) {
47
52
  mergeAssistantMessageEvent(existing, node);
@@ -153,8 +158,12 @@ function applyRenderSequenceToHistoryNode(byId, storedEvent, event) {
153
158
  && typeof event.toolCallId === "string"
154
159
  && node.toolCallId === event.toolCallId
155
160
  && (!eventId || !node.eventId || node.eventId === eventId)));
156
- if (target)
157
- applyRenderSequence(target, storedEvent, event);
161
+ if (!target)
162
+ return;
163
+ applyRenderSequence(target, storedEvent, event);
164
+ if (event.type === "tool_execution_finished" && event.toolMetrics) {
165
+ target.toolMetrics = event.toolMetrics;
166
+ }
158
167
  }
159
168
  function applyRenderSequence(node, storedEvent, event) {
160
169
  const renderSequence = storedEvent.renderSequence ?? event.renderSequence;
@@ -171,6 +180,9 @@ function applyRenderSequence(node, storedEvent, event) {
171
180
  function applyStoredPayloadRef(node, event, storedPayloadRef) {
172
181
  if (!storedPayloadRef)
173
182
  return;
183
+ if (event.type === "assistant_message" || event.type === "thinking_finished") {
184
+ node.payloadRefs = { ...node.payloadRefs, [storedPayloadRef.payloadKind ?? "output"]: storedPayloadRef };
185
+ }
174
186
  if (event.type === "tool_call" || event.type === "tool_execution_started") {
175
187
  node.payloadRefs = { ...node.payloadRefs, input: storedPayloadRef };
176
188
  }
@@ -524,6 +536,57 @@ export function latestTraceStreamId(events, initial) {
524
536
  }
525
537
  return latest;
526
538
  }
539
+ function attachModelInferenceToLatestOutput(nodes, byId, event, storedEvent) {
540
+ const eventId = event.eventId;
541
+ const candidates = flattenTraceNodes(nodes)
542
+ .filter((node) => node.eventId === eventId && (event.inferenceTarget || traceNodeStartedBeforeInference(node, storedEvent)) && (node.type === "assistant.message"
543
+ || node.type === "model.reasoning"
544
+ || node.type === "tool.call"
545
+ || node.type === "agent.delegation"))
546
+ .sort(compareTraceNodes);
547
+ const anchor = event.inferenceTarget;
548
+ const turnNode = eventId ? byId.get(messageTurnNodeId(eventId)) : undefined;
549
+ const target = anchor
550
+ ? (anchor.type === "tool"
551
+ ? candidates.find((node) => node.toolCallId === anchor.toolCallId)
552
+ : anchor.type === "assistant"
553
+ ? candidates.find((node) => node.stableKey === `assistant:${eventId}:assistant:${anchor.assistantIndex}`)
554
+ : turnNode) ?? turnNode
555
+ : candidates.at(-1) ?? turnNode;
556
+ if (!target)
557
+ return;
558
+ const id = event.inferenceId
559
+ ? `${eventId ?? storedEvent.piboSessionId}:inference:${event.inferenceId}`
560
+ : eventId ? `${eventId}:usage:${event.usageIndex ?? 0}` : storedEvent.id;
561
+ const record = {
562
+ id,
563
+ completedAt: storedEvent.createdAt,
564
+ metrics: {
565
+ ...(event.inputTokens === undefined ? {} : { inputTokens: event.inputTokens }),
566
+ ...(event.outputTokens === undefined ? {} : { outputTokens: event.outputTokens }),
567
+ ...(event.cacheReadTokens === undefined ? {} : { cacheReadTokens: event.cacheReadTokens }),
568
+ ...(event.cacheWriteTokens === undefined ? {} : { cacheWriteTokens: event.cacheWriteTokens }),
569
+ ...(event.reasoningTokens === undefined ? {} : { reasoningTokens: event.reasoningTokens }),
570
+ totalTokens: event.totalTokens,
571
+ ...(event.costUsd === undefined ? {} : { costUsd: event.costUsd }),
572
+ },
573
+ };
574
+ target.modelInferences = [...(target.modelInferences ?? []).filter((item) => item.id !== id), record];
575
+ }
576
+ function traceNodeStartedBeforeInference(node, storedEvent) {
577
+ const inferenceRenderSequence = storedEvent.renderSequence;
578
+ const nodeRenderSequence = node.orderKey?.renderSequence;
579
+ if (inferenceRenderSequence !== undefined && nodeRenderSequence !== undefined) {
580
+ return nodeRenderSequence <= inferenceRenderSequence;
581
+ }
582
+ const inferenceSequence = storedEvent.eventSequence ?? storedEvent.streamId;
583
+ const nodeSequence = node.orderKey?.eventSequence ?? node.orderKey?.streamId;
584
+ if (inferenceSequence !== undefined && nodeSequence !== undefined)
585
+ return nodeSequence <= inferenceSequence;
586
+ const inferenceTime = Date.parse(storedEvent.createdAt);
587
+ const nodeTime = Date.parse(node.startedAt ?? node.completedAt ?? "");
588
+ return !Number.isFinite(inferenceTime) || !Number.isFinite(nodeTime) || nodeTime <= inferenceTime;
589
+ }
527
590
  // ── event → node helpers ─────────────────────────────────────────
528
591
  function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByToolCallId, sessionStatus, createdAt, eventSequence, streamId, streamFrameIndex, traceSource, storedInstanceId) {
529
592
  const eventId = "eventId" in event && typeof event.eventId === "string" ? event.eventId : undefined;
@@ -562,6 +625,7 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
562
625
  type: "user.message",
563
626
  title: "User Message",
564
627
  status: isOptimisticUserMessageEvent(event) ? "running" : "done",
628
+ messageDeliveryState: isOptimisticUserMessageEvent(event) ? "sending" : undefined,
565
629
  summary: event.text,
566
630
  output: event.text,
567
631
  };
@@ -711,6 +775,7 @@ function traceNodeFromEvent(piboSessionId, event, childByParent, linkedChildByTo
711
775
  input: { reason: event.reason },
712
776
  output: event.result,
713
777
  error: event.errorMessage,
778
+ compactionStats: event.compactionStats,
714
779
  stableKey: `compaction:${eventInstanceKey}`,
715
780
  };
716
781
  }
@@ -768,6 +833,7 @@ function findMatchingContentNode(byId, update) {
768
833
  return [...byId.values()].find((candidate) => candidate.type === update.type && candidate.stableKey === update.stableKey);
769
834
  }
770
835
  function mergeAssistantMessageEvent(target, update) {
836
+ target.payloadRefs = { ...target.payloadRefs, ...update.payloadRefs };
771
837
  target.status = update.status;
772
838
  target.summary = update.summary ?? target.summary;
773
839
  target.output = update.output ?? target.output;
@@ -1163,6 +1229,7 @@ function mergeCompactionEvent(target, update) {
1163
1229
  target.input = update.input ?? target.input;
1164
1230
  target.output = update.output ?? target.output;
1165
1231
  target.error = update.error ?? target.error;
1232
+ target.compactionStats = update.compactionStats ?? target.compactionStats;
1166
1233
  target.completedAt = update.completedAt ?? target.completedAt;
1167
1234
  }
1168
1235
  function hasVisibleText(value) {
@@ -710,6 +710,7 @@ function createUserMessageNode(piboSessionId, entry, entryIndex, timing, fallbac
710
710
  startedAt: entry.createdAt,
711
711
  summary: text,
712
712
  output: text,
713
+ payloadRefs: historyContentPayloadRef(entry),
713
714
  error: entry.error,
714
715
  source,
715
716
  stableKey: eventIdentity ?? historyEntryStableKey(entry),
@@ -902,6 +903,7 @@ function createAssistantMessageNode(input) {
902
903
  completedAt: input.completedAt,
903
904
  summary: input.text,
904
905
  output: input.text,
906
+ payloadRefs: historyContentPayloadRef(input.entry),
905
907
  error: input.error,
906
908
  source,
907
909
  stableKey: eventIdentity ? `assistant:${eventIdentity}` : `${historyEntryStableKey(input.entry)}:response:${input.contentPartIndex}`,
@@ -909,6 +911,13 @@ function createAssistantMessageNode(input) {
909
911
  children: input.children ?? [],
910
912
  };
911
913
  }
914
+ /** Preserve a stored full body when the bounded history page contains its preview. */
915
+ function historyContentPayloadRef(entry) {
916
+ const ref = entry.source === "product" ? entry.metadata?.tracePayloadRef : undefined;
917
+ if (!ref || typeof ref !== "object" || Array.isArray(ref) || typeof ref.ref !== "string" || typeof ref.byteLength !== "number" || typeof ref.preview !== "string")
918
+ return undefined;
919
+ return { output: ref };
920
+ }
912
921
  function historyMessageParts(entry) {
913
922
  return typeof entry.content === "string" ? [{ type: "text", text: entry.content }] : [...entry.content];
914
923
  }
@@ -165,6 +165,7 @@ function makeStored(streamEvent, piboSessionId, type, payload, nextSequence, now
165
165
  streamId: typeof streamEvent.streamId === "number" ? streamEvent.streamId : undefined,
166
166
  streamFrameIndex,
167
167
  traceSource: "live",
168
+ storedPayloadRef: streamEvent.storedPayloadRef,
168
169
  eventId: typeof positionedPayload.eventId === "string" ? positionedPayload.eventId : undefined,
169
170
  type,
170
171
  createdAt: streamEventCreatedAt(streamEvent) ?? now(),
@@ -64,6 +64,7 @@ function traceNodeShallowEqual(left, right) {
64
64
  left.toolMetrics?.inputTokens === right.toolMetrics?.inputTokens &&
65
65
  left.toolMetrics?.outputTokens === right.toolMetrics?.outputTokens &&
66
66
  left.toolMetrics?.tokenBasis === right.toolMetrics?.tokenBasis &&
67
+ modelInferenceRecordsEqual(left.modelInferences, right.modelInferences) &&
67
68
  left.summary === right.summary &&
68
69
  left.input === right.input &&
69
70
  left.output === right.output &&
@@ -73,6 +74,24 @@ function traceNodeShallowEqual(left, right) {
73
74
  left.stableKey === right.stableKey &&
74
75
  traceOrderKeyEqual(left.orderKey, right.orderKey));
75
76
  }
77
+ function modelInferenceRecordsEqual(left, right) {
78
+ if (left === right)
79
+ return true;
80
+ if (!left || !right || left.length !== right.length)
81
+ return false;
82
+ return left.every((item, index) => {
83
+ const other = right[index];
84
+ return item.id === other?.id
85
+ && item.completedAt === other.completedAt
86
+ && item.metrics.inputTokens === other.metrics.inputTokens
87
+ && item.metrics.outputTokens === other.metrics.outputTokens
88
+ && item.metrics.cacheReadTokens === other.metrics.cacheReadTokens
89
+ && item.metrics.cacheWriteTokens === other.metrics.cacheWriteTokens
90
+ && item.metrics.reasoningTokens === other.metrics.reasoningTokens
91
+ && item.metrics.totalTokens === other.metrics.totalTokens
92
+ && item.metrics.costUsd === other.metrics.costUsd;
93
+ });
94
+ }
76
95
  function traceOrderKeyEqual(left, right) {
77
96
  if (left === right)
78
97
  return true;