@pasko70/pibo 3.5.1 → 3.6.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.
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="theme-color" content="#101d22" />
7
7
  <title>Pibo</title>
8
- <script type="module" crossorigin src="/apps/chat-vscode/assets/index-DZgW1fCB.js"></script>
8
+ <script type="module" crossorigin src="/apps/chat-vscode/assets/index-CxKxukk_.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-b18ZkEo0.css">
10
10
  </head>
11
11
  <body>
@@ -0,0 +1,136 @@
1
+ import { cacheUsageWarningText } from "../shared/cache-observability.js";
2
+ import { compareInferenceCompletion, modelInferenceCacheReadRatio, modelInferenceCachedInputTokens, modelInferenceInputTokens, modelInferenceUncachedInputTokens, } from "../shared/model-inference-metrics.js";
3
+ import { inspectDebugTrace } from "./trace.js";
4
+ import { formatNextCommands } from "./next-commands.js";
5
+ function finiteToken(value) {
6
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : undefined;
7
+ }
8
+ export async function inspectDebugCache(piboSessionId, stores, options = {}) {
9
+ const trace = await inspectDebugTrace(piboSessionId, stores);
10
+ const byId = new Map();
11
+ for (const node of trace.nodes) {
12
+ for (const record of node.modelInferences ?? [])
13
+ byId.set(record.id, { nodeId: node.id, nodeTitle: node.title, record });
14
+ }
15
+ const located = [...byId.values()].sort((left, right) => compareInferenceCompletion(left.record, right.record));
16
+ let reportedInput = 0;
17
+ let reportedRead = 0;
18
+ let reportedCount = 0;
19
+ let unreportedCount = 0;
20
+ let writeTotal = 0;
21
+ let writeReported = false;
22
+ let possibleDropCount = 0;
23
+ for (const { record } of located) {
24
+ const input = modelInferenceInputTokens(record.metrics);
25
+ const read = modelInferenceCachedInputTokens(record.metrics);
26
+ if (input !== undefined && input > 0 && read !== undefined && read <= input) {
27
+ reportedInput += input;
28
+ reportedRead += read;
29
+ reportedCount++;
30
+ }
31
+ else {
32
+ unreportedCount++;
33
+ }
34
+ const write = finiteToken(record.metrics.cacheWriteTokens);
35
+ if (write !== undefined) {
36
+ writeTotal += write;
37
+ writeReported = true;
38
+ }
39
+ if (record.cacheObservation?.warning === "possible-cache-read-drop")
40
+ possibleDropCount++;
41
+ }
42
+ const limit = options.limit ?? 20;
43
+ const selected = located.slice(Math.max(0, located.length - limit));
44
+ return {
45
+ piboSessionId,
46
+ runtimeInstanceId: trace.runtimeInstanceId,
47
+ runtimeAdapterId: trace.runtimeAdapterId,
48
+ source: "provider-reported-usage",
49
+ summary: {
50
+ inferenceCount: located.length,
51
+ cacheReadReportedCount: reportedCount,
52
+ cacheReadUnreportedCount: unreportedCount,
53
+ possibleDropCount,
54
+ ...(reportedCount ? {
55
+ inputTokens: reportedInput,
56
+ cacheReadTokens: reportedRead,
57
+ uncachedInputTokens: reportedInput - reportedRead,
58
+ cacheReadRatio: reportedRead / reportedInput,
59
+ } : {}),
60
+ ...(writeReported ? { cacheWriteTokens: writeTotal } : {}),
61
+ },
62
+ inferences: selected.map(({ nodeId, nodeTitle, record }) => ({
63
+ id: record.id,
64
+ nodeId,
65
+ nodeTitle,
66
+ completedAt: record.completedAt,
67
+ inputTokens: modelInferenceInputTokens(record.metrics),
68
+ cacheReadTokens: modelInferenceCachedInputTokens(record.metrics),
69
+ cacheWriteTokens: finiteToken(record.metrics.cacheWriteTokens),
70
+ uncachedInputTokens: modelInferenceUncachedInputTokens(record.metrics),
71
+ cacheReadRatio: modelInferenceCacheReadRatio(record.metrics),
72
+ previousCacheReadRatio: record.cacheObservation?.previousCacheReadRatio,
73
+ elapsedMs: record.cacheObservation?.elapsedMs,
74
+ cacheState: record.cacheObservation?.cacheState ?? "unknown",
75
+ warning: record.cacheObservation?.warning ?? "none",
76
+ warningText: record.cacheObservation ? cacheUsageWarningText(record.cacheObservation) : undefined,
77
+ explanation: record.cacheObservation?.explanation ?? "insufficient-data",
78
+ })),
79
+ limitations: [
80
+ "Cache counters are reported by the runtime or provider; missing counters remain unknown.",
81
+ "A cache-read drop does not identify whether Pibo, the runtime, the provider, or eviction caused it.",
82
+ "This command does not inspect or alter runtime source code, prompts, or provider cache keys.",
83
+ ],
84
+ nextCommands: [
85
+ `pibo debug trace ${piboSessionId} --medium`,
86
+ `pibo debug events ${piboSessionId} --type assistant_usage --fields inputTokens,cacheReadTokens,cacheWriteTokens,totalTokens`,
87
+ ],
88
+ };
89
+ }
90
+ function metric(value) {
91
+ return value === undefined ? "unknown" : String(value);
92
+ }
93
+ function ratio(value) {
94
+ return value === undefined ? "unknown" : `${(value * 100).toFixed(1)}%`;
95
+ }
96
+ export function formatDebugCache(result) {
97
+ const lines = [
98
+ `piboSessionId: ${result.piboSessionId}`,
99
+ ...(result.runtimeInstanceId ? [`runtimeInstanceId: ${result.runtimeInstanceId}`] : []),
100
+ ...(result.runtimeAdapterId ? [`runtimeAdapterId: ${result.runtimeAdapterId}`] : []),
101
+ `source: ${result.source}`,
102
+ `inferences: ${result.summary.inferenceCount}`,
103
+ `cacheReadReported: ${result.summary.cacheReadReportedCount}`,
104
+ `cacheReadUnreported: ${result.summary.cacheReadUnreportedCount}`,
105
+ `possibleDrops: ${result.summary.possibleDropCount}`,
106
+ `inputTokens: ${metric(result.summary.inputTokens)}`,
107
+ `cacheReadTokens: ${metric(result.summary.cacheReadTokens)}`,
108
+ `uncachedInputTokens: ${metric(result.summary.uncachedInputTokens)}`,
109
+ `cacheWriteTokens: ${metric(result.summary.cacheWriteTokens)}`,
110
+ `cacheReadRatio: ${ratio(result.summary.cacheReadRatio)}`,
111
+ "",
112
+ ];
113
+ if (result.inferences.length) {
114
+ lines.push("completedAt\tstate\tinput\tcacheRead\tuncached\tcacheWrite\tratio\tid\tnode");
115
+ for (const inference of result.inferences) {
116
+ lines.push([
117
+ inference.completedAt ?? "unknown",
118
+ inference.cacheState,
119
+ metric(inference.inputTokens),
120
+ metric(inference.cacheReadTokens),
121
+ metric(inference.uncachedInputTokens),
122
+ metric(inference.cacheWriteTokens),
123
+ ratio(inference.cacheReadRatio),
124
+ inference.id,
125
+ inference.nodeTitle,
126
+ ].join("\t"));
127
+ }
128
+ }
129
+ for (const inference of result.inferences) {
130
+ if (inference.warningText)
131
+ lines.push(`cache-warning\t${inference.id}\t${inference.warningText}`);
132
+ }
133
+ lines.push("", "Limitations:", ...result.limitations.map((item) => `- ${item}`));
134
+ lines.push(...formatNextCommands(result.nextCommands));
135
+ return lines.join("\n");
136
+ }
@@ -34,6 +34,10 @@ export async function runDebugCli(argv = process.argv) {
34
34
  await runDebugTrace(args.slice(1));
35
35
  return;
36
36
  }
37
+ if (args[0] === "cache") {
38
+ await runDebugCache(args.slice(1));
39
+ return;
40
+ }
37
41
  if (args[0] === "summary") {
38
42
  await runDebugSummary(args.slice(1));
39
43
  return;
@@ -815,6 +819,29 @@ async function runDebugTelemetry(args) {
815
819
  }
816
820
  throw new Error(`Unknown pibo debug telemetry command "${command}". Run pibo debug telemetry --help.`);
817
821
  }
822
+ async function runDebugCache(args) {
823
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
824
+ printDebugCacheDiscovery();
825
+ return;
826
+ }
827
+ const options = parseOptions(args);
828
+ const piboSessionId = options.positionals[0];
829
+ if (!piboSessionId)
830
+ throw new Error("pibo debug cache requires <pibo-session-id>");
831
+ if (options.positionals.length > 1)
832
+ throw new Error("pibo debug cache accepts one <pibo-session-id>. Run pibo debug cache --help.");
833
+ const limit = options.limit === undefined ? undefined : Number(options.limit);
834
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1 || limit > 200))
835
+ throw new Error("--limit must be an integer from 1 to 200");
836
+ const stores = { sessions: resolveDebugStore("sessions"), chat: resolveDebugStore("chat") };
837
+ const { formatJson } = await import("./sql.js");
838
+ const { formatDebugCache, inspectDebugCache } = await import("./cache.js");
839
+ const result = await inspectDebugCache(piboSessionId, stores, { limit });
840
+ if (options.json)
841
+ console.log(formatJson(result));
842
+ else
843
+ console.log(formatDebugCache(result));
844
+ }
818
845
  async function runDebugTrace(args) {
819
846
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
820
847
  printDebugTraceDiscovery();
@@ -1514,6 +1541,7 @@ Commands:
1514
1541
  messages List or show stored user/assistant messages
1515
1542
  final Show the latest assistant message
1516
1543
  trace Rebuild the Chat Web trace view for one Pibo Session
1544
+ cache Summarize provider-reported cache usage for one Pibo Session
1517
1545
  events Inspect compact event payload fields for one Pibo Session
1518
1546
  agents Inspect delegated child agents and their persisted activity
1519
1547
  tool Inspect one grouped tool call
@@ -1536,6 +1564,7 @@ Next:
1536
1564
  pibo debug final <pibo-session-id>
1537
1565
  pibo debug messages <pibo-session-id> list
1538
1566
  pibo debug trace <pibo-session-id> --running-only
1567
+ pibo debug cache <pibo-session-id>
1539
1568
  pibo debug events stream --topic pibo.output
1540
1569
  pibo debug persistence
1541
1570
  pibo debug repair output <pibo-session-id> <event-id> --dry-run
@@ -1807,6 +1836,23 @@ Next:
1807
1836
  pibo debug tool ps_... <tool-call-id> --output
1808
1837
  `);
1809
1838
  }
1839
+ function printDebugCacheDiscovery() {
1840
+ console.log(`pibo debug cache - summarize provider-reported cache usage
1841
+
1842
+ Usage:
1843
+ pibo debug cache <pibo-session-id> [--limit n] [--json]
1844
+
1845
+ Reports:
1846
+ Per-inference input, cache-read, cache-write and uncached token counts.
1847
+ Warns when large consecutive requests move from mostly cached to mostly uncached input.
1848
+ Provider metrics cannot identify the cause of a cache-read drop.
1849
+
1850
+ Next:
1851
+ pibo debug cache ps_... --json
1852
+ pibo debug trace ps_... --medium
1853
+ pibo debug events ps_... --type assistant_usage --fields inputTokens,cacheReadTokens,cacheWriteTokens,totalTokens
1854
+ `);
1855
+ }
1810
1856
  function printDebugTraceDiscovery() {
1811
1857
  console.log(`pibo debug trace - rebuild one Chat Web trace view
1812
1858
 
@@ -48,6 +48,7 @@ export function inspectDebugSession(input, stores, options = {}) {
48
48
  `pibo debug messages ${parsed.piboSessionId} list`,
49
49
  `pibo debug final ${parsed.piboSessionId}`,
50
50
  `pibo debug trace ${parsed.piboSessionId} --check`,
51
+ `pibo debug cache ${parsed.piboSessionId}`,
51
52
  `pibo debug failures ${parsed.piboSessionId}`,
52
53
  `pibo debug events ${parsed.piboSessionId} --limit 20`,
53
54
  ];
@@ -114,6 +115,7 @@ export function inspectDebugSessionRuntime(input, stores) {
114
115
  nextCommands: [
115
116
  `pibo debug trace ${parsed.piboSessionId} --check`,
116
117
  `pibo debug trace ${parsed.piboSessionId} --native-history --check`,
118
+ `pibo debug cache ${parsed.piboSessionId}`,
117
119
  `pibo debug events ${parsed.piboSessionId} --limit 20`,
118
120
  ],
119
121
  };
@@ -42,6 +42,7 @@ export async function inspectDebugSummary(input, stores) {
42
42
  `pibo debug failures ${parsed.piboSessionId}`,
43
43
  `pibo debug messages ${parsed.piboSessionId} list`,
44
44
  `pibo debug trace ${parsed.piboSessionId} --check`,
45
+ `pibo debug cache ${parsed.piboSessionId}`,
45
46
  `pibo debug events ${parsed.piboSessionId} --limit 20`,
46
47
  ],
47
48
  };
@@ -7,6 +7,8 @@ import { compareTraceNodes } from "../shared/trace-nodes.js";
7
7
  import { openReadOnlyDebugDatabase, withStorePath } from "./sql.js";
8
8
  import { formatNextCommands } from "./next-commands.js";
9
9
  import { resolveDebugTraceSessionStatus, summarizeDebugTraceStatus } from "./trace-status.js";
10
+ import { cacheUsageWarningText } from "../shared/cache-observability.js";
11
+ import { modelInferenceCacheReadRatio, modelInferenceCachedInputTokens, modelInferenceInputTokens, modelInferenceUncachedInputTokens, } from "../shared/model-inference-metrics.js";
10
12
  export async function inspectDebugTrace(piboSessionId, stores, options = {}) {
11
13
  if (!stores.sessions.exists)
12
14
  throw new Error(`Debug store "sessions" not found at ${stores.sessions.path}`);
@@ -113,6 +115,23 @@ export async function inspectDebugTraceNode(piboSessionId, stores, nodeId) {
113
115
  nextCommands: node ? buildNodeNextCommands(piboSessionId, node) : [`pibo debug trace ${piboSessionId}`],
114
116
  };
115
117
  }
118
+ function formatMetric(value) {
119
+ return value === undefined ? "unknown" : String(value);
120
+ }
121
+ function formatRatio(value) {
122
+ return value === undefined ? "unknown" : `${(value * 100).toFixed(1)}%`;
123
+ }
124
+ function formatModelInference(record) {
125
+ return [
126
+ record.id,
127
+ `input=${formatMetric(modelInferenceInputTokens(record.metrics))}`,
128
+ `cacheRead=${formatMetric(modelInferenceCachedInputTokens(record.metrics))}`,
129
+ `uncached=${formatMetric(modelInferenceUncachedInputTokens(record.metrics))}`,
130
+ `cacheWrite=${formatMetric(record.metrics.cacheWriteTokens)}`,
131
+ `output=${formatMetric(record.metrics.outputTokens)}`,
132
+ `cacheReadRatio=${formatRatio(modelInferenceCacheReadRatio(record.metrics))}`,
133
+ ].join("\t");
134
+ }
116
135
  export function formatDebugTrace(result, options = {}) {
117
136
  const lines = [
118
137
  `piboSessionId: ${result.piboSessionId}`,
@@ -152,6 +171,12 @@ export function formatDebugTrace(result, options = {}) {
152
171
  order: node.order,
153
172
  };
154
173
  lines.push(columns.map((column) => values[column] ?? "").join("\t"));
174
+ for (const inference of node.modelInferences ?? []) {
175
+ lines.push(`model-inference\t${formatModelInference(inference)}`);
176
+ const warning = inference.cacheObservation && cacheUsageWarningText(inference.cacheObservation);
177
+ if (warning)
178
+ lines.push(`cache-warning\t${inference.id}\t${warning}`);
179
+ }
155
180
  }
156
181
  lines.push(`nodes: ${result.nodes.length}${result.nodes.length !== result.rawNodeCount ? ` of ${result.rawNodeCount}` : ""}`);
157
182
  if (result.checks) {
@@ -194,6 +219,8 @@ export function formatDebugTraceNode(result) {
194
219
  lines.push(`runId: ${node.runId}`);
195
220
  if (node.toolCallId)
196
221
  lines.push(`toolCallId: ${node.toolCallId}`);
222
+ for (const inference of node.modelInferences ?? [])
223
+ lines.push(`modelInference: ${JSON.stringify(inference)}`);
197
224
  lines.push(...formatNextCommands(result.nextCommands));
198
225
  return lines.join("\n");
199
226
  }
@@ -214,6 +241,7 @@ function flattenTraceNodes(nodes, depth = 0) {
214
241
  startedAt: node.startedAt,
215
242
  completedAt: node.completedAt,
216
243
  childrenCount: node.children.length,
244
+ ...(node.modelInferences?.length ? { modelInferences: node.modelInferences } : {}),
217
245
  depth,
218
246
  },
219
247
  ...flattenTraceNodes(node.children, depth + 1),
@@ -23,10 +23,11 @@ export function buildCompactTerminalRows(traceView, options) {
23
23
  const candidates = syncThinkingToolRows(flatNodes.map((item) => createRowCandidate(item.node, item.turnId)));
24
24
  applyCompletedTurnTiming(candidates, turnById);
25
25
  const reconciled = reconcileConceptualRowCandidates(candidates);
26
- const rows = (options.toolDisplayMode ?? "default") === "default"
27
- ? groupRelatedToolCandidates(reconciled, showToolDebugMetrics).map((candidate) => candidate.row)
26
+ const toolDisplayMode = options.toolDisplayMode ?? "default";
27
+ const rows = toolDisplayMode === "default" || toolDisplayMode === "slim"
28
+ ? groupRelatedToolCandidates(reconciled, showToolDebugMetrics || toolDisplayMode === "slim").map((candidate) => candidate.row)
28
29
  : reconciled.map((candidate) => candidate.row);
29
- return applyToolDisplayMode(rows, options.toolDisplayMode ?? "default");
30
+ return applyToolDisplayMode(rows, toolDisplayMode);
30
31
  }
31
32
  function applyToolDisplayMode(rows, mode) {
32
33
  if (mode === "default")
@@ -1041,7 +1042,7 @@ function createImageGroup(candidates) {
1041
1042
  lines: [
1042
1043
  {
1043
1044
  prefix: "bullet",
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")],
1045
+ tokens: [token(status === "running" ? `Viewing ${detailItems.length} images` : status === "error" ? `${detailItems.length} image reads · error` : `${detailItems.length} Viewed ${detailItems.length === 1 ? "Image" : "Images"}`, toneForStatus(status), "semibold")],
1045
1046
  },
1046
1047
  ...visibleDetailItems.map((item, index) => ({
1047
1048
  prefix: index === 0 ? "detail" : "continuation",
@@ -0,0 +1,73 @@
1
+ import { modelInferenceCachedInputTokens, modelInferenceInputTokens, modelInferenceUncachedInputTokens, } from "./model-inference-metrics.js";
2
+ export const CACHE_READ_DROP_RULE = "provider-cache-read-drop-v1";
3
+ export const CACHE_READ_DROP_MIN_INPUT_TOKENS = 16_384;
4
+ function cacheState(ratio) {
5
+ if (ratio === undefined)
6
+ return "unknown";
7
+ if (ratio <= 0.1)
8
+ return "cold";
9
+ if (ratio >= 0.8)
10
+ return "warm";
11
+ return "partial";
12
+ }
13
+ function elapsedMs(current, previous) {
14
+ if (!previous?.completedAt || !current.completedAt)
15
+ return undefined;
16
+ const before = Date.parse(previous.completedAt);
17
+ const after = Date.parse(current.completedAt);
18
+ return Number.isFinite(before) && Number.isFinite(after) && after >= before ? after - before : undefined;
19
+ }
20
+ /**
21
+ * Compares bounded provider usage counters. It does not inspect prompts, infer a
22
+ * provider cache key, or identify why a cache read changed.
23
+ */
24
+ export function observeCacheUsage(current, previous, options = {}) {
25
+ const inputTokens = modelInferenceInputTokens(current.metrics);
26
+ const cacheReadTokens = modelInferenceCachedInputTokens(current.metrics);
27
+ const uncachedInputTokens = modelInferenceUncachedInputTokens(current.metrics);
28
+ const cacheWriteTokens = Number.isFinite(current.metrics.cacheWriteTokens) && Number(current.metrics.cacheWriteTokens) >= 0
29
+ ? Math.floor(Number(current.metrics.cacheWriteTokens))
30
+ : undefined;
31
+ const ratio = inputTokens !== undefined && inputTokens > 0 && cacheReadTokens !== undefined && cacheReadTokens <= inputTokens
32
+ ? cacheReadTokens / inputTokens
33
+ : undefined;
34
+ const previousInput = modelInferenceInputTokens(previous?.metrics);
35
+ const previousRead = modelInferenceCachedInputTokens(previous?.metrics);
36
+ const previousRatio = previousInput !== undefined && previousInput > 0 && previousRead !== undefined && previousRead <= previousInput
37
+ ? previousRead / previousInput
38
+ : undefined;
39
+ const elapsed = elapsedMs(current, previous);
40
+ const observation = {
41
+ rule: CACHE_READ_DROP_RULE,
42
+ source: "provider-reported-usage",
43
+ cacheState: cacheState(ratio),
44
+ warning: "none",
45
+ explanation: ratio === undefined ? "insufficient-data" : "provider-usage",
46
+ ...(inputTokens === undefined ? {} : { inputTokens }),
47
+ ...(cacheReadTokens === undefined ? {} : { cacheReadTokens }),
48
+ ...(cacheWriteTokens === undefined ? {} : { cacheWriteTokens }),
49
+ ...(uncachedInputTokens === undefined ? {} : { uncachedInputTokens }),
50
+ ...(ratio === undefined ? {} : { cacheReadRatio: ratio }),
51
+ ...(previous ? { previousInferenceId: previous.id } : {}),
52
+ ...(previousRatio === undefined ? {} : { previousCacheReadRatio: previousRatio }),
53
+ ...(elapsed === undefined ? {} : { elapsedMs: elapsed }),
54
+ };
55
+ if (options.compactionBetween) {
56
+ observation.explanation = "compaction-between-inferences";
57
+ return observation;
58
+ }
59
+ if (inputTokens !== undefined && previousInput !== undefined && cacheReadTokens !== undefined && previousRead !== undefined
60
+ && inputTokens >= CACHE_READ_DROP_MIN_INPUT_TOKENS && previousInput >= CACHE_READ_DROP_MIN_INPUT_TOKENS
61
+ && ratio !== undefined && previousRatio !== undefined && previousRatio >= 0.8 && ratio <= 0.1 && cacheReadTokens < previousRead) {
62
+ observation.warning = "possible-cache-read-drop";
63
+ observation.explanation = "cache-read-ratio-dropped";
64
+ }
65
+ return observation;
66
+ }
67
+ export function cacheUsageWarningText(observation) {
68
+ if (observation.warning === "none")
69
+ return undefined;
70
+ const current = observation.cacheReadRatio === undefined ? "unknown" : `${(observation.cacheReadRatio * 100).toFixed(1)}%`;
71
+ const previous = observation.previousCacheReadRatio === undefined ? "unknown" : `${(observation.previousCacheReadRatio * 100).toFixed(1)}%`;
72
+ return `Possible cache-read drop: provider-reported cached input fell from ${previous} to ${current}. The provider metrics do not identify the cause.`;
73
+ }
@@ -6,18 +6,31 @@ export function modelInferenceInputTokens(metrics) {
6
6
  return tokenCount(metrics?.inputTokens);
7
7
  }
8
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);
9
+ return tokenCount(metrics?.cacheReadTokens);
14
10
  }
15
11
  export function modelInferenceUncachedInputTokens(metrics) {
16
12
  const input = modelInferenceInputTokens(metrics);
17
- if (input === undefined)
13
+ const cached = modelInferenceCachedInputTokens(metrics);
14
+ if (input === undefined || cached === undefined || cached > input)
18
15
  return undefined;
19
- return Math.max(0, input - (modelInferenceCachedInputTokens(metrics) ?? 0));
16
+ return input - cached;
17
+ }
18
+ export function modelInferenceCacheReadRatio(metrics) {
19
+ const input = modelInferenceInputTokens(metrics);
20
+ const cached = modelInferenceCachedInputTokens(metrics);
21
+ if (input === undefined || input === 0 || cached === undefined || cached > input)
22
+ return undefined;
23
+ return cached / input;
20
24
  }
21
25
  function tokenCount(value) {
22
26
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : undefined;
23
27
  }
28
+ export function compareInferenceCompletion(left, right) {
29
+ const time = Date.parse(left.completedAt ?? "") - Date.parse(right.completedAt ?? "");
30
+ if (Number.isFinite(time) && time !== 0)
31
+ return time;
32
+ if (left.completedSequence !== undefined && right.completedSequence !== undefined) {
33
+ return left.completedSequence - right.completedSequence;
34
+ }
35
+ return 0;
36
+ }
@@ -4,6 +4,8 @@ import { attachAsyncAgentRunNode, isRunStartToolNode } from "./trace-async-agent
4
4
  import { createRunNotificationNode, parseRunNotificationText } from "./trace-run-notifications.js";
5
5
  import { findLikelyTraceChildSession, isSubagentToolName, } from "./trace-subagent-links.js";
6
6
  import { qualifiedToolNodeId } from "./trace-tool-identity.js";
7
+ import { observeCacheUsage } from "./cache-observability.js";
8
+ import { compareInferenceCompletion } from "./model-inference-metrics.js";
7
9
  export function applySingleEventToNodes(nodes, byId, piboSessionId, storedEvent, childByParent, linkedChildByToolCallId, historyCoverage, openTranscriptEventIds, sessionStatus) {
8
10
  const payload = storedEvent.payload;
9
11
  if (payload.type === "assistant_usage") {
@@ -538,7 +540,11 @@ export function latestTraceStreamId(events, initial) {
538
540
  }
539
541
  function attachModelInferenceToLatestOutput(nodes, byId, event, storedEvent) {
540
542
  const eventId = event.eventId;
541
- const candidates = flattenTraceNodes(nodes)
543
+ const flattened = flattenTraceNodes(nodes);
544
+ const id = event.inferenceId
545
+ ? `${eventId ?? storedEvent.piboSessionId}:inference:${event.inferenceId}`
546
+ : eventId ? `${eventId}:usage:${event.usageIndex ?? 0}` : storedEvent.id;
547
+ const candidates = flattened
542
548
  .filter((node) => node.eventId === eventId && (event.inferenceTarget || traceNodeStartedBeforeInference(node, storedEvent)) && (node.type === "assistant.message"
543
549
  || node.type === "model.reasoning"
544
550
  || node.type === "tool.call"
@@ -546,21 +552,23 @@ function attachModelInferenceToLatestOutput(nodes, byId, event, storedEvent) {
546
552
  .sort(compareTraceNodes);
547
553
  const anchor = event.inferenceTarget;
548
554
  const turnNode = eventId ? byId.get(messageTurnNodeId(eventId)) : undefined;
549
- const target = anchor
555
+ const anchoredTarget = anchor
550
556
  ? (anchor.type === "tool"
551
557
  ? candidates.find((node) => node.toolCallId === anchor.toolCallId)
552
558
  : anchor.type === "assistant"
553
559
  ? candidates.find((node) => node.stableKey === `assistant:${eventId}:assistant:${anchor.assistantIndex}`)
554
560
  : turnNode) ?? turnNode
555
561
  : candidates.at(-1) ?? turnNode;
562
+ // A repeated provider receipt updates its original inference instead of moving
563
+ // the usage record to whichever output happened to render last.
564
+ const target = flattened.find((node) => node.modelInferences?.some((item) => item.id === id)) ?? anchoredTarget;
556
565
  if (!target)
557
566
  return;
558
- const id = event.inferenceId
559
- ? `${eventId ?? storedEvent.piboSessionId}:inference:${event.inferenceId}`
560
- : eventId ? `${eventId}:usage:${event.usageIndex ?? 0}` : storedEvent.id;
567
+ const existingRecord = target.modelInferences?.find((item) => item.id === id);
561
568
  const record = {
562
569
  id,
563
- completedAt: storedEvent.createdAt,
570
+ completedAt: existingRecord?.completedAt ?? storedEvent.createdAt,
571
+ completedSequence: existingRecord?.completedSequence ?? storedEvent.eventSequence ?? storedEvent.streamId,
564
572
  metrics: {
565
573
  ...(event.inputTokens === undefined ? {} : { inputTokens: event.inputTokens }),
566
574
  ...(event.outputTokens === undefined ? {} : { outputTokens: event.outputTokens }),
@@ -571,7 +579,27 @@ function attachModelInferenceToLatestOutput(nodes, byId, event, storedEvent) {
571
579
  ...(event.costUsd === undefined ? {} : { costUsd: event.costUsd }),
572
580
  },
573
581
  };
574
- target.modelInferences = [...(target.modelInferences ?? []).filter((item) => item.id !== id), record];
582
+ const currentAt = Date.parse(record.completedAt ?? "");
583
+ let previous;
584
+ for (const node of flattened) {
585
+ for (const item of node.modelInferences ?? []) {
586
+ if (item.id !== id && compareInferenceCompletion(item, record) < 0
587
+ && (!previous || compareInferenceCompletion(item, previous) > 0)) {
588
+ previous = item;
589
+ }
590
+ }
591
+ }
592
+ const compactionBetween = previous !== undefined && Number.isFinite(currentAt) && flattened.some((node) => {
593
+ if (node.type !== "execution.compaction")
594
+ return false;
595
+ const boundary = {
596
+ completedAt: node.startedAt ?? node.completedAt,
597
+ completedSequence: node.orderKey?.eventSequence ?? node.orderKey?.streamId,
598
+ };
599
+ return compareInferenceCompletion(boundary, previous) > 0 && compareInferenceCompletion(boundary, record) <= 0;
600
+ });
601
+ record.cacheObservation = observeCacheUsage(record, previous, { compactionBetween });
602
+ target.modelInferences = [...(target.modelInferences ?? []).filter((item) => item.id !== id), record].sort(compareInferenceCompletion);
575
603
  }
576
604
  function traceNodeStartedBeforeInference(node, storedEvent) {
577
605
  const inferenceRenderSequence = storedEvent.renderSequence;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.5.1",
3
+ "version": "3.6.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@pasko70/pibo",
9
- "version": "3.5.1",
9
+ "version": "3.6.1",
10
10
  "workspaces": [
11
11
  "packages/workflows"
12
12
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pasko70/pibo",
3
- "version": "3.5.1",
3
+ "version": "3.6.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",