pi-editor-footer 0.6.1 → 0.6.2

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.2] - 2026-08-25
8
+
9
+ ### Fixed
10
+
11
+ - Live `↑` no longer exceeds context window or session total — `telemetry:peekAgentLive` and `endAgent` now use peak window (`max`) for `inputTokens` not sum (summing `50k+60k=110k` double-counted overlapping history `> 60k` window), `live-border` top `↑` now shows current window `peekLive` + per-agent `output`/`cost` capped to `contextUsage.tokens`, idle also via telemetry `max` capped, timeline `↑` prefers telemetry `max` capped
12
+
7
13
  ## [0.6.1] - 2026-08-25
8
14
 
9
15
  ### Fixed
package/dist/index.js CHANGED
@@ -631,7 +631,7 @@ export default function (pi) {
631
631
  pi.on("turn_start", (e, ctx) => {
632
632
  // Input is known at turn_start via context usage — seed live input so peekLive shows it during streaming (output already streams via liveDeltaChars)
633
633
  const usageTokens = ctx // SAFETY: pi context seam — getContextUsage is ExtensionContext API
634
- ?.getContextUsage?.()?.tokens;
634
+ ?.getContextUsage?.()?.tokens;
635
635
  if (typeof usageTokens === "number" &&
636
636
  Number.isFinite(usageTokens) &&
637
637
  usageTokens > 0) {
@@ -715,21 +715,29 @@ export default function (pi) {
715
715
  const cacheRate = totals.latestCacheHitRate ?? 0;
716
716
  const cacheStr = `${glyphs.cacheHit} ${cacheRate.toFixed(1)}%`;
717
717
  // Respect timeline.* toggles for specified metrics (wallTime/tokens/cost), but datetime/cache/turn/tools are always shown per user spec
718
- // Timeline tokens per-agent: prefer baseline delta (totals - baseline) which yields 18k (279k-261k) not 279k total.
719
- // Fallback to tel (tracker sum) then session totals.
718
+ // Timeline tokens per-agent: input is peak window (max), not sum summing full prompts
719
+ // double-counts overlapping history (50k+60k=110k > window 60k). Prefer telemetry max
720
+ // (tel.inputTokens after fix) when available, else delta capped to context/total.
721
+ // Output/cost remain per-agent sum for billing.
720
722
  let telInput;
721
723
  let telOutput;
722
724
  let telCost;
723
- if (agentBaselineTotals) {
724
- telInput = Math.max(0, totals.input - agentBaselineTotals.input);
725
- telOutput = Math.max(0, totals.output - agentBaselineTotals.output);
726
- telCost = Math.max(0, totals.cost - agentBaselineTotals.cost);
727
- }
728
- else if (tel) {
725
+ if (tel) {
729
726
  telInput = tel.inputTokens;
730
727
  telOutput = tel.outputTokens;
731
728
  telCost = tel.costUsd;
732
729
  }
730
+ else if (agentBaselineTotals) {
731
+ telInput = Math.max(0, totals.input - agentBaselineTotals.input);
732
+ telOutput = Math.max(0, totals.output - agentBaselineTotals.output);
733
+ telCost = Math.max(0, totals.cost - agentBaselineTotals.cost);
734
+ // Cap input to not exceed context window (peak) or session total
735
+ const ctxTokens = lastSessionCtx?.getContextUsage?.()?.tokens;
736
+ if (typeof ctxTokens === "number" && Number.isFinite(ctxTokens))
737
+ telInput = Math.min(telInput, ctxTokens);
738
+ if (totals.input > 0)
739
+ telInput = Math.min(telInput, totals.input);
740
+ }
733
741
  else {
734
742
  telInput = totals.input;
735
743
  telOutput = totals.output;
@@ -197,38 +197,85 @@ export class LiveBorder {
197
197
  let tokensText = "";
198
198
  if (cfg.telemetry.enabled && cfg.telemetry.tokens) {
199
199
  const isRunning = this.deps.runActivityTracker.isRunning();
200
- // When idle (settled) and baseline available, show per-agent delta (input/output/cost) = cur totals - baseline at agent_start.
201
- // This yields 18k for the example (279k session - 261k baseline = 18k agent) instead of 279k total.
202
- // When running, use live agent tracker (sum of turns in this agent + live turn) which is already per-agent after guard fix.
200
+ // Live input: window occupancy (max), not sum. Summing full prompts double-counts
201
+ // overlapping history (50k+60k=110k > window 60k) and exceeds context/total.
202
+ // Idle shows per-agent max via telemetry (fallback to delta capped), running shows
203
+ // current turn window (peekLive) + per-agent output/cost, both capped to context/total.
203
204
  if (!isRunning && this.agentBaseline) {
204
205
  const cur = snapshot.totals;
205
206
  const base = this.agentBaseline;
206
- const deltaInput = Math.max(0, cur.input - base.input);
207
- const deltaOutput = Math.max(0, cur.output - base.output);
208
- // Build minimal telemetry for formatting (only tokens matter for formatTelemetryTokens)
207
+ // Idle per-agent display — input is peak window (max), not sum, to avoid
208
+ // double-count exceed (sum of prompts double-counts overlapping history).
209
+ // Prefer telemetry max when available, else delta capped to context/totals.
210
+ const trackerIdle = this.deps.telemetryTracker;
211
+ const telIdle = trackerIdle.peekAgentLive() ?? trackerIdle.getLastTelemetry();
212
+ let displayInput;
213
+ let displayOutput;
214
+ let displayCost;
215
+ if (telIdle) {
216
+ displayInput = telIdle.inputTokens;
217
+ displayOutput = telIdle.outputTokens;
218
+ displayCost = telIdle.costUsd;
219
+ }
220
+ else {
221
+ displayInput = Math.max(0, cur.input - base.input);
222
+ displayOutput = Math.max(0, cur.output - base.output);
223
+ displayCost = Math.max(0, cur.cost - base.cost);
224
+ }
225
+ // Guarantee invariant: live input never exceeds context window or session total
226
+ if (snapshot.contextUsage?.tokens)
227
+ displayInput = Math.min(displayInput, snapshot.contextUsage.tokens);
228
+ if (cur.input > 0)
229
+ displayInput = Math.min(displayInput, cur.input);
209
230
  const deltaTel = {
210
231
  tps: null,
211
232
  ttftMs: 0,
212
233
  totalMs: 0,
213
- inputTokens: deltaInput,
214
- outputTokens: deltaOutput,
234
+ inputTokens: displayInput,
235
+ outputTokens: displayOutput,
215
236
  stallMs: 0,
216
237
  stallCount: 0,
217
238
  rateUsdPerMTokens: null,
218
239
  generationMs: 0,
219
- totalTokens: deltaInput + deltaOutput,
220
- costUsd: Math.max(0, cur.cost - base.cost),
240
+ totalTokens: displayInput + displayOutput,
241
+ costUsd: displayCost,
221
242
  measurementMs: null,
222
243
  };
223
244
  tokensText = formatTelemetryTokens(deltaTel, theme, cfg.telemetry, glyphs);
224
245
  }
225
246
  else {
226
- // Live or fallback — per-agent sum from tracker (option B), correctly reset per agent_start
227
- // SAFETY: pi seam intentional unsafe cast, validated at runtime — telemetry tracker for top tokens line (agent run)
247
+ // Live per-agent input is current window (peekLive), not per-agent sum,
248
+ // to avoid sum(50k+60k)=110k > window 60k. Output/cost still per-agent sum.
228
249
  const tracker = this.deps.telemetryTracker;
229
- const live = tracker.peekAgentLive() ?? tracker.getLastTelemetry();
230
- if (live) {
231
- tokensText = formatTelemetryTokens(live, theme, cfg.telemetry, glyphs);
250
+ const agentLive = tracker.peekAgentLive() ?? tracker.getLastTelemetry();
251
+ const liveTurn = tracker.peekLive();
252
+ let displayLive = agentLive;
253
+ if (agentLive &&
254
+ liveTurn &&
255
+ this.deps.runActivityTracker.isRunning()) {
256
+ // Input = current turn window (liveTurn), output/cost = per-agent sum
257
+ displayLive = {
258
+ ...agentLive,
259
+ inputTokens: liveTurn.inputTokens,
260
+ totalTokens: liveTurn.inputTokens + agentLive.outputTokens,
261
+ };
262
+ }
263
+ if (displayLive) {
264
+ let cappedInput = displayLive.inputTokens;
265
+ if (snapshot.contextUsage?.tokens)
266
+ cappedInput = Math.min(cappedInput, snapshot.contextUsage.tokens);
267
+ // Note: not capping to session totals during running — totals is authoritative
268
+ // (lagging) while liveTurn is predictive (current window). Capping to totals
269
+ // would make live stale (50k) during second turn streaming instead of showing
270
+ // current window 60k. After fix to max, live 60k == context 60k, not exceed context;
271
+ // live may still be > authoritative totals interim (60k > 50k) but will be <= totals
272
+ // after turn completes (60k <= 110k). This is expected predictive vs authoritative.
273
+ displayLive = {
274
+ ...displayLive,
275
+ inputTokens: cappedInput,
276
+ totalTokens: cappedInput + displayLive.outputTokens,
277
+ };
278
+ tokensText = formatTelemetryTokens(displayLive, theme, cfg.telemetry, glyphs);
232
279
  }
233
280
  }
234
281
  }
package/dist/telemetry.js CHANGED
@@ -83,11 +83,12 @@ export class TurnTelemetryTracker {
83
83
  let stallCount = 0;
84
84
  let generationMs = 0;
85
85
  let ttftMs = 0;
86
- // sum completed turns
86
+ // input per turn is the full prompt (includes history), summing double-counts overlapping
87
+ // history and makes live input exceed context usage (e.g. 50k+60k=110k > window 60k).
88
+ // Display input as peak window occupancy (max), not sum. Output/cost still sum.
87
89
  for (const t of this.agentTurns) {
88
- inputTokens += t.inputTokens;
90
+ inputTokens = Math.max(inputTokens, t.inputTokens);
89
91
  outputTokens += t.outputTokens;
90
- totalTokens += t.totalTokens;
91
92
  costUsd += t.costUsd;
92
93
  stallMs += t.stallMs;
93
94
  stallCount += t.stallCount;
@@ -96,9 +97,8 @@ export class TurnTelemetryTracker {
96
97
  if (this.agentTurns.length > 0)
97
98
  ttftMs = this.agentTurns[0].ttftMs;
98
99
  if (live) {
99
- inputTokens += live.inputTokens;
100
+ inputTokens = Math.max(inputTokens, live.inputTokens);
100
101
  outputTokens += live.outputTokens;
101
- totalTokens += live.totalTokens;
102
102
  costUsd += live.costUsd;
103
103
  stallMs += live.stallMs;
104
104
  stallCount += live.stallCount;
@@ -106,6 +106,8 @@ export class TurnTelemetryTracker {
106
106
  if (ttftMs === 0)
107
107
  ttftMs = live.ttftMs;
108
108
  }
109
+ // totalTokens is window input (max) + cumulative output, not sum of per-turn totals
110
+ totalTokens = inputTokens + outputTokens;
109
111
  const now = this.now();
110
112
  const totalMs = Math.max(0, now - this.agentStartMs);
111
113
  const measurementMs = outputTokens > 0 && generationMs > 0 ? generationMs : null;
@@ -397,8 +399,9 @@ export class TurnTelemetryTracker {
397
399
  if (startMs === null || turns.length === 0)
398
400
  return;
399
401
  const outputTokens = turns.reduce((sum, t) => sum + t.outputTokens, 0);
400
- const inputTokens = turns.reduce((sum, t) => sum + t.inputTokens, 0);
401
- const totalTokens = turns.reduce((sum, t) => sum + t.totalTokens, 0);
402
+ const inputTokens = turns.reduce((sum, t) => Math.max(sum, t.inputTokens), 0);
403
+ // totalTokens is window input (max) + cumulative output, not sum of per-turn totals
404
+ const totalTokens = inputTokens + outputTokens;
402
405
  const costUsd = turns.reduce((sum, t) => sum + t.costUsd, 0);
403
406
  const stallMs = turns.reduce((sum, t) => sum + t.stallMs, 0);
404
407
  const stallCount = turns.reduce((sum, t) => sum + t.stallCount, 0);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-editor-footer",
3
3
  "type": "module",
4
- "description": "Pi TUI theme project-aware footer, model border, and skill detail window (TrackingEditor, live theme)",
4
+ "description": "Pi TUI theme \u2014 project-aware footer, model border, and skill detail window (TrackingEditor, live theme)",
5
5
  "scripts": {
6
6
  "test": "node --import tsx --test test/*.test.ts",
7
7
  "typecheck": "tsc --noEmit",
@@ -18,7 +18,7 @@
18
18
  "@earendil-works/pi-coding-agent": "*",
19
19
  "@earendil-works/pi-tui": "*"
20
20
  },
21
- "version": "0.6.1",
21
+ "version": "0.6.2",
22
22
  "files": [
23
23
  "dist",
24
24
  "src",
package/src/index.ts CHANGED
@@ -802,9 +802,8 @@ export default function (pi: ExtensionAPILike): void {
802
802
  pi.on("turn_start", (e, ctx) => {
803
803
  // Input is known at turn_start via context usage — seed live input so peekLive shows it during streaming (output already streams via liveDeltaChars)
804
804
  const usageTokens = (
805
- ctx as unknown as { getContextUsage?: () => { tokens?: number } }
806
- ) // SAFETY: pi context seam — getContextUsage is ExtensionContext API
807
- ?.getContextUsage?.()?.tokens;
805
+ ctx as unknown as { getContextUsage?: () => { tokens?: number } } // SAFETY: pi context seam — getContextUsage is ExtensionContext API
806
+ )?.getContextUsage?.()?.tokens;
808
807
  if (
809
808
  typeof usageTokens === "number" &&
810
809
  Number.isFinite(usageTokens) &&
@@ -895,24 +894,34 @@ export default function (pi: ExtensionAPILike): void {
895
894
  const cacheRate = totals.latestCacheHitRate ?? 0;
896
895
  const cacheStr = `${glyphs.cacheHit} ${cacheRate.toFixed(1)}%`;
897
896
  // Respect timeline.* toggles for specified metrics (wallTime/tokens/cost), but datetime/cache/turn/tools are always shown per user spec
898
- // Timeline tokens per-agent: prefer baseline delta (totals - baseline) which yields 18k (279k-261k) not 279k total.
899
- // Fallback to tel (tracker sum) then session totals.
900
- let telInput: number;
901
- let telOutput: number;
902
- let telCost: number;
903
- if (agentBaselineTotals) {
904
- telInput = Math.max(0, totals.input - agentBaselineTotals.input);
905
- telOutput = Math.max(0, totals.output - agentBaselineTotals.output);
906
- telCost = Math.max(0, totals.cost - agentBaselineTotals.cost);
907
- } else if (tel) {
908
- telInput = tel.inputTokens;
909
- telOutput = tel.outputTokens;
910
- telCost = tel.costUsd;
911
- } else {
912
- telInput = totals.input;
913
- telOutput = totals.output;
914
- telCost = totals.cost;
915
- }
897
+ // Timeline tokens per-agent: input is peak window (max), not sum summing full prompts
898
+ // double-counts overlapping history (50k+60k=110k > window 60k). Prefer telemetry max
899
+ // (tel.inputTokens after fix) when available, else delta capped to context/total.
900
+ // Output/cost remain per-agent sum for billing.
901
+ let telInput: number;
902
+ let telOutput: number;
903
+ let telCost: number;
904
+ if (tel) {
905
+ telInput = tel.inputTokens;
906
+ telOutput = tel.outputTokens;
907
+ telCost = tel.costUsd;
908
+ } else if (agentBaselineTotals) {
909
+ telInput = Math.max(0, totals.input - agentBaselineTotals.input);
910
+ telOutput = Math.max(0, totals.output - agentBaselineTotals.output);
911
+ telCost = Math.max(0, totals.cost - agentBaselineTotals.cost);
912
+ // Cap input to not exceed context window (peak) or session total
913
+ const ctxTokens = (
914
+ lastSessionCtx as unknown as { // SAFETY: pi seam — intentional unsafe cast, validated at runtime
915
+ getContextUsage?: () => { tokens?: number };
916
+ }
917
+ )?.getContextUsage?.()?.tokens;
918
+ if (typeof ctxTokens === "number" && Number.isFinite(ctxTokens)) telInput = Math.min(telInput, ctxTokens);
919
+ if (totals.input > 0) telInput = Math.min(telInput, totals.input);
920
+ } else {
921
+ telInput = totals.input;
922
+ telOutput = totals.output;
923
+ telCost = totals.cost;
924
+ }
916
925
  const line1Parts: string[] = [dt];
917
926
  if (currentConfig.timeline.wallTime) line1Parts.push(wallDur);
918
927
  else line1Parts.push(wallDur); // wall time always per spec (11s)
@@ -245,27 +245,50 @@ export class LiveBorder {
245
245
  let tokensText = "";
246
246
  if (cfg.telemetry.enabled && cfg.telemetry.tokens) {
247
247
  const isRunning = this.deps.runActivityTracker.isRunning();
248
- // When idle (settled) and baseline available, show per-agent delta (input/output/cost) = cur totals - baseline at agent_start.
249
- // This yields 18k for the example (279k session - 261k baseline = 18k agent) instead of 279k total.
250
- // When running, use live agent tracker (sum of turns in this agent + live turn) which is already per-agent after guard fix.
248
+ // Live input: window occupancy (max), not sum. Summing full prompts double-counts
249
+ // overlapping history (50k+60k=110k > window 60k) and exceeds context/total.
250
+ // Idle shows per-agent max via telemetry (fallback to delta capped), running shows
251
+ // current turn window (peekLive) + per-agent output/cost, both capped to context/total.
251
252
  if (!isRunning && this.agentBaseline) {
252
253
  const cur = snapshot.totals;
253
254
  const base = this.agentBaseline;
254
- const deltaInput = Math.max(0, cur.input - base.input);
255
- const deltaOutput = Math.max(0, cur.output - base.output);
256
- // Build minimal telemetry for formatting (only tokens matter for formatTelemetryTokens)
255
+ // Idle per-agent display — input is peak window (max), not sum, to avoid
256
+ // double-count exceed (sum of prompts double-counts overlapping history).
257
+ // Prefer telemetry max when available, else delta capped to context/totals.
258
+ const trackerIdle = this.deps.telemetryTracker as unknown as {
259
+ peekAgentLive(): import("./telemetry.js").TurnTelemetry | null;
260
+ getLastTelemetry(): import("./telemetry.js").TurnTelemetry | null;
261
+ };
262
+ const telIdle =
263
+ trackerIdle.peekAgentLive() ?? trackerIdle.getLastTelemetry();
264
+ let displayInput: number;
265
+ let displayOutput: number;
266
+ let displayCost: number;
267
+ if (telIdle) {
268
+ displayInput = telIdle.inputTokens;
269
+ displayOutput = telIdle.outputTokens;
270
+ displayCost = telIdle.costUsd;
271
+ } else {
272
+ displayInput = Math.max(0, cur.input - base.input);
273
+ displayOutput = Math.max(0, cur.output - base.output);
274
+ displayCost = Math.max(0, cur.cost - base.cost);
275
+ }
276
+ // Guarantee invariant: live input never exceeds context window or session total
277
+ if (snapshot.contextUsage?.tokens)
278
+ displayInput = Math.min(displayInput, snapshot.contextUsage.tokens);
279
+ if (cur.input > 0) displayInput = Math.min(displayInput, cur.input);
257
280
  const deltaTel: TurnTelemetry = {
258
281
  tps: null,
259
282
  ttftMs: 0,
260
283
  totalMs: 0,
261
- inputTokens: deltaInput,
262
- outputTokens: deltaOutput,
284
+ inputTokens: displayInput,
285
+ outputTokens: displayOutput,
263
286
  stallMs: 0,
264
287
  stallCount: 0,
265
288
  rateUsdPerMTokens: null,
266
289
  generationMs: 0,
267
- totalTokens: deltaInput + deltaOutput,
268
- costUsd: Math.max(0, cur.cost - base.cost),
290
+ totalTokens: displayInput + displayOutput,
291
+ costUsd: displayCost,
269
292
  measurementMs: null,
270
293
  };
271
294
  tokensText = formatTelemetryTokens(
@@ -275,16 +298,46 @@ export class LiveBorder {
275
298
  glyphs as never,
276
299
  );
277
300
  } else {
278
- // Live or fallback — per-agent sum from tracker (option B), correctly reset per agent_start
279
- // SAFETY: pi seam intentional unsafe cast, validated at runtime — telemetry tracker for top tokens line (agent run)
301
+ // Live per-agent input is current window (peekLive), not per-agent sum,
302
+ // to avoid sum(50k+60k)=110k > window 60k. Output/cost still per-agent sum.
280
303
  const tracker = this.deps.telemetryTracker as unknown as {
281
304
  peekAgentLive(): import("./telemetry.js").TurnTelemetry | null;
305
+ peekLive(): import("./telemetry.js").TurnTelemetry | null;
282
306
  getLastTelemetry(): import("./telemetry.js").TurnTelemetry | null;
283
307
  };
284
- const live = tracker.peekAgentLive() ?? tracker.getLastTelemetry();
285
- if (live) {
308
+ const agentLive =
309
+ tracker.peekAgentLive() ?? tracker.getLastTelemetry();
310
+ const liveTurn = tracker.peekLive();
311
+ let displayLive = agentLive;
312
+ if (
313
+ agentLive &&
314
+ liveTurn &&
315
+ this.deps.runActivityTracker.isRunning()
316
+ ) {
317
+ // Input = current turn window (liveTurn), output/cost = per-agent sum
318
+ displayLive = {
319
+ ...agentLive,
320
+ inputTokens: liveTurn.inputTokens,
321
+ totalTokens: liveTurn.inputTokens + agentLive.outputTokens,
322
+ };
323
+ }
324
+ if (displayLive) {
325
+ let cappedInput = displayLive.inputTokens;
326
+ if (snapshot.contextUsage?.tokens)
327
+ cappedInput = Math.min(cappedInput, snapshot.contextUsage.tokens);
328
+ // Note: not capping to session totals during running — totals is authoritative
329
+ // (lagging) while liveTurn is predictive (current window). Capping to totals
330
+ // would make live stale (50k) during second turn streaming instead of showing
331
+ // current window 60k. After fix to max, live 60k == context 60k, not exceed context;
332
+ // live may still be > authoritative totals interim (60k > 50k) but will be <= totals
333
+ // after turn completes (60k <= 110k). This is expected predictive vs authoritative.
334
+ displayLive = {
335
+ ...displayLive,
336
+ inputTokens: cappedInput,
337
+ totalTokens: cappedInput + displayLive.outputTokens,
338
+ };
286
339
  tokensText = formatTelemetryTokens(
287
- live,
340
+ displayLive,
288
341
  theme as never,
289
342
  cfg.telemetry,
290
343
  glyphs as never,
package/src/telemetry.ts CHANGED
@@ -185,11 +185,12 @@ export class TurnTelemetryTracker {
185
185
  let stallCount = 0;
186
186
  let generationMs = 0;
187
187
  let ttftMs = 0;
188
- // sum completed turns
188
+ // input per turn is the full prompt (includes history), summing double-counts overlapping
189
+ // history and makes live input exceed context usage (e.g. 50k+60k=110k > window 60k).
190
+ // Display input as peak window occupancy (max), not sum. Output/cost still sum.
189
191
  for (const t of this.agentTurns) {
190
- inputTokens += t.inputTokens;
192
+ inputTokens = Math.max(inputTokens, t.inputTokens);
191
193
  outputTokens += t.outputTokens;
192
- totalTokens += t.totalTokens;
193
194
  costUsd += t.costUsd;
194
195
  stallMs += t.stallMs;
195
196
  stallCount += t.stallCount;
@@ -197,15 +198,16 @@ export class TurnTelemetryTracker {
197
198
  }
198
199
  if (this.agentTurns.length > 0) ttftMs = this.agentTurns[0]!.ttftMs;
199
200
  if (live) {
200
- inputTokens += live.inputTokens;
201
+ inputTokens = Math.max(inputTokens, live.inputTokens);
201
202
  outputTokens += live.outputTokens;
202
- totalTokens += live.totalTokens;
203
203
  costUsd += live.costUsd;
204
204
  stallMs += live.stallMs;
205
205
  stallCount += live.stallCount;
206
206
  generationMs += live.generationMs;
207
207
  if (ttftMs === 0) ttftMs = live.ttftMs;
208
208
  }
209
+ // totalTokens is window input (max) + cumulative output, not sum of per-turn totals
210
+ totalTokens = inputTokens + outputTokens;
209
211
  const now = this.now();
210
212
  const totalMs = Math.max(0, now - this.agentStartMs);
211
213
  const measurementMs =
@@ -524,8 +526,12 @@ export class TurnTelemetryTracker {
524
526
  if (startMs === null || turns.length === 0) return;
525
527
 
526
528
  const outputTokens = turns.reduce((sum, t) => sum + t.outputTokens, 0);
527
- const inputTokens = turns.reduce((sum, t) => sum + t.inputTokens, 0);
528
- const totalTokens = turns.reduce((sum, t) => sum + t.totalTokens, 0);
529
+ const inputTokens = turns.reduce(
530
+ (sum, t) => Math.max(sum, t.inputTokens),
531
+ 0,
532
+ );
533
+ // totalTokens is window input (max) + cumulative output, not sum of per-turn totals
534
+ const totalTokens = inputTokens + outputTokens;
529
535
  const costUsd = turns.reduce((sum, t) => sum + t.costUsd, 0);
530
536
  const stallMs = turns.reduce((sum, t) => sum + t.stallMs, 0);
531
537
  const stallCount = turns.reduce((sum, t) => sum + t.stallCount, 0);