@klars/agentobs 0.2.1 → 0.2.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.
@@ -135,14 +135,19 @@ export async function importTranscript(db, file) {
135
135
  const message = (row.message ?? {});
136
136
  const usage = message.usage;
137
137
  if (usage && typeof usage === 'object') {
138
- // Only genuinely fresh input tokens go in tokensIn. Cache writes and
139
- // reads are tracked separately because they bill at different rates,
140
- // and because a cache read replays the whole context every turn - the
141
- // headline token count must not include the same tokens hundreds of
142
- // times.
143
- result.tokensIn += usage.input_tokens ?? 0;
138
+ // Fresh input = uncached input + cache writes. A cache write is real
139
+ // new content being sent for the first time (just stored for reuse), so
140
+ // excluding it made "tokens in" absurd: 24K in against 4.6M out, when
141
+ // real agent usage is heavily input-weighted. It is still tracked
142
+ // separately for costing, since it bills at 1.25x.
143
+ //
144
+ // Cache *reads* stay out of this total: they replay the entire context
145
+ // on every turn, so counting them would report the same tokens hundreds
146
+ // of times (2.4 billion across three sessions).
147
+ const cacheWrite = usage.cache_creation_input_tokens ?? 0;
148
+ result.tokensIn += (usage.input_tokens ?? 0) + cacheWrite;
144
149
  result.tokensOut += usage.output_tokens ?? 0;
145
- result.cacheWriteTokens += usage.cache_creation_input_tokens ?? 0;
150
+ result.cacheWriteTokens += cacheWrite;
146
151
  // cache_read is the whole conversation context replayed on every single
147
152
  // message, so it re-counts the same tokens on each turn - summing it
148
153
  // reported 413 million tokens for one session. It is tracked separately
@@ -186,6 +191,8 @@ export async function importTranscript(db, file) {
186
191
  }
187
192
  if (!sessionStarted)
188
193
  return result;
194
+ // tokensIn already contains the cache-write tokens, so they are costed once
195
+ // at the base rate here and only topped up by the extra 0.25x premium.
189
196
  const baseCost = computeCost(result.model, result.tokensIn, result.tokensOut);
190
197
  const readCost = computeCost(result.model, result.cacheReadTokens, 0);
191
198
  const writeCost = computeCost(result.model, result.cacheWriteTokens, 0);
@@ -194,7 +201,7 @@ export async function importTranscript(db, file) {
194
201
  ? null
195
202
  : baseCost +
196
203
  (readCost ?? 0) * CACHE_READ_RATE +
197
- (writeCost ?? 0) * CACHE_WRITE_RATE;
204
+ (writeCost ?? 0) * (CACHE_WRITE_RATE - 1);
198
205
  // Session totals come from the transcript's own usage blocks, which are
199
206
  // authoritative - the per-call rows have no tokens to sum.
200
207
  db.prepare(`UPDATE sessions
@@ -191,10 +191,10 @@ function renderTools(rows) {
191
191
  body.replaceChildren();
192
192
  if (rows.length === 0) {
193
193
  const tr = document.createElement('tr');
194
- const msg =
195
- state.summary && state.summary.coarse_sessions > 0
196
- ? 'Coarse sessions record no tool calls — connect the Claude Code hook for per-tool detail.'
197
- : 'No tool calls recorded yet.';
194
+ const msg =
195
+ state.summary && state.summary.coarse_sessions > 0
196
+ ? 'Coarse sessions record no tool calls — connect the Claude Code hook for per-tool detail.'
197
+ : 'No tool calls recorded yet.';
198
198
  tr.append(Object.assign(cell(msg, 'empty'), { colSpan: 5 }));
199
199
  body.append(tr);
200
200
  return;
@@ -630,6 +630,16 @@ function renderDelta(el, current, previous, { goodWhenUp = true } = {}) {
630
630
  return;
631
631
  }
632
632
  const change = ((current - previous) / previous) * 100;
633
+ // A tiny previous period produces a huge, meaningless percentage - "+2300%"
634
+ // says nothing except that yesterday was nearly empty. Cap the display so
635
+ // the chip stays informative rather than theatrical.
636
+ if (Math.abs(change) > 999) {
637
+ el.className = `delta ${change > 0 === goodWhenUp ? 'delta-good' : 'delta-bad'}`;
638
+ el.textContent = `${change > 0 ? '▲' : '▼'} vs ${previous}`;
639
+ el.title = `Previous period had only ${previous}; a percentage would be misleading`;
640
+ el.removeAttribute('hidden');
641
+ return;
642
+ }
633
643
  if (!Number.isFinite(change) || Math.abs(change) < 0.5) {
634
644
  // Below half a percent is noise; a chip there implies a signal that
635
645
  // isn't real.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@klars/agentobs",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Observability and control layer for AI coding agents - see every tool call, token, and dollar your agents spend, and stop them before they do something risky.",
5
5
  "license": "MIT",
6
6
  "author": "Klars AI",