@chroxify/eunoe 1.2.0 → 1.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chroxify/eunoe",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Context management for Claude Code that never forgets what matters.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -200,24 +200,24 @@ export const replay: Command = {
200
200
  ["Cost", `${c.green(c.bold(`${fewer(sum.plainCost, sum.eunoeCost)} cheaper`))} ${c.dim("cache reads at 0.1×, writes at 1.25×")}`],
201
201
  ["Folds", c.bold(sum.folds.toLocaleString())],
202
202
  ["Folds dropped", `${c.bold(sum.dropped.toLocaleString())} ${c.dim("folded turns removed entirely to fit the window")}`],
203
- ["Peak context", `plain ${tokens(sum.peakPlain)} ${c.dim("→")} eunoe ${c.bold(tokens(sum.peakEunoe))}`],
203
+ ["Peak context", `plain ${c.green(c.bold(tokens(sum.peakPlain)))} ${c.dim("→")} eunoe ${c.bold(tokens(sum.peakEunoe))} ${c.dim("· the longest conversation whole, every compaction undone, against the most eunoe sent")}`],
204
204
  ],
205
205
  4,
206
206
  )
207
207
  const saved = [...compared]
208
208
  .filter((r) => r.nativeCompactions > 0)
209
- .sort((a, b) => b.discarded - a.discarded)
209
+ .sort((a, b) => b.peakPlain - a.peakPlain)
210
210
  .slice(0, TOP_SESSIONS)
211
211
  if (saved.length > 0) {
212
212
  line()
213
- line(` ${c.dim("Sessions Claude Code would have compacted, by history it would have lost")}`)
213
+ line(` ${c.dim("Longest sessions Claude Code would have compacted, by their whole history")}`)
214
214
  table(
215
215
  saved.map((r) => [
216
216
  c.cyan(shortId(r.id)),
217
217
  (r.title ?? (r.cwd ? tilde(r.cwd) : "")).slice(0, 40),
218
- c.bold(c.green(`${plural(r.nativeCompactions, "compaction")} avoided`)),
219
- `${tokens(r.discarded)} tokens kept`,
220
- c.dim(`${plural(r.folds, "fold")} · ${tokens(r.plainTokens)} → ${tokens(r.eunoeTokens)} tokens`),
218
+ `${c.green(c.bold(tokens(r.peakPlain)))} ${c.dim("of history")}`,
219
+ c.bold(`${plural(r.nativeCompactions, "compaction")} avoided`),
220
+ c.dim(`eunoe peak ${tokens(r.peakEunoe)} · ${plural(r.folds, "fold")}`),
221
221
  ]),
222
222
  )
223
223
  }
@@ -2,7 +2,9 @@ import { claudeConfigDirs, readConfig } from "../../config"
2
2
  import type { Command } from "../command"
3
3
  import { cleanupDays } from "../proxy/claude-settings"
4
4
  import { type RequestRecord, recentRequests } from "../proxy/inspect"
5
- import { savedTokens, totals } from "../proxy/stats"
5
+ import { nativeCompactions, savedTokens, type ThreadPeak, threadPeaks, totals } from "../proxy/stats"
6
+ import { REPLAY_NATIVE_COMPACT_AT } from "../replay"
7
+ import { shortId } from "../sessions"
6
8
  import { c, fail, line, percent, plural, table, tokens, wrap } from "../terminal/ui"
7
9
  import { costOf, ratesOf, readUsage, type UsageEntry, usageFiles } from "../usage"
8
10
 
@@ -20,6 +22,7 @@ const MATCH_WITHIN = 10 * 60_000
20
22
  const NOTE_WIDTH = 110
21
23
  const DEFAULT_DAYS = 7
22
24
  const DEFAULT_WEEKS = 8
25
+ const LONGEST = 5
23
26
 
24
27
  const midnight = (ms: number) => new Date(new Date(ms).toDateString()).getTime()
25
28
  const addDays = (ms: number, days: number) => {
@@ -123,7 +126,13 @@ function notSentCell(logged: RequestRecord[]) {
123
126
  const sum = totals(logged)
124
127
  if (sum.requests === 0) return c.dim("–")
125
128
  if (sum.saved <= 0) return c.dim("±0")
126
- return `${tokens(sum.saved)} ${c.dim(plural(sum.folds, "fold"))}`
129
+ return `${c.green(c.bold(tokens(sum.saved)))} ${c.dim(plural(sum.folds, "fold"))}`
130
+ }
131
+
132
+ function avoidedCell(logged: RequestRecord[], avoided: Set<RequestRecord>) {
133
+ if (!logged.some((record) => record.input)) return c.dim("–")
134
+ const count = logged.filter((record) => avoided.has(record)).length
135
+ return count > 0 ? c.green(c.bold(count.toLocaleString())) : c.dim("0")
127
136
  }
128
137
 
129
138
  function costSavedCell(logged: RequestRecord[], matched: Matched) {
@@ -131,13 +140,13 @@ function costSavedCell(logged: RequestRecord[], matched: Matched) {
131
140
  if (sum.priced === 0) return c.dim("–")
132
141
  if (Math.abs(sum.saved) < 0.005) return c.dim("±0")
133
142
  const share = c.dim(percent(sum.saved / (sum.actual + sum.saved)))
134
- return sum.saved > 0 ? `${c.green(c.bold(dollars(sum.saved)))} ${share}` : `${c.yellow(`−${dollars(-sum.saved)}`)} ${share}`
143
+ return sum.saved > 0 ? `${dollars(sum.saved)} ${share}` : `${c.yellow(`−${dollars(-sum.saved)}`)} ${share}`
135
144
  }
136
145
 
137
- function row(label: string, usage: UsageEntry[], logged: RequestRecord[], matched: Matched, bold = false) {
146
+ function row(label: string, usage: UsageEntry[], logged: RequestRecord[], matched: Matched, avoided: Set<RequestRecord>, bold = false) {
138
147
  const sum = summarize(usage)
139
148
  const strong = (text: string) => (bold ? c.bold(text) : text)
140
- const eunoe = [notSentCell(logged), costSavedCell(logged, matched)]
149
+ const eunoe = [avoidedCell(logged, avoided), notSentCell(logged), costSavedCell(logged, matched)]
141
150
  if (sum.requests === 0) return [strong(label), c.dim("–"), "", "", "", "", "", ...eunoe]
142
151
  return [strong(label), strong(sum.requests.toLocaleString()), tokens(sum.input), tokens(sum.output), tokens(sum.cacheWrite), tokens(sum.cacheRead), strong(`${dollars(sum.cost)}${sum.unpriced ? c.dim("*") : ""}`), ...eunoe]
143
152
  }
@@ -161,6 +170,15 @@ function byModel(usage: UsageEntry[]) {
161
170
  ])
162
171
  }
163
172
 
173
+ function longestRow(peak: ThreadPeak) {
174
+ return [
175
+ c.cyan(shortId(peak.session)),
176
+ `${c.green(c.bold(tokens(peak.plain)))} ${c.dim("of history")}`,
177
+ peak.compactions > 0 ? `${c.bold(plural(peak.compactions, "compaction"))} avoided` : c.dim("no compaction needed"),
178
+ c.dim(`eunoe sent ${tokens(peak.sent)}${peak.turns ? ` · ${plural(peak.turns, "turn")}` : ""} · ${dayLabel(Date.parse(peak.lastSeen))}`),
179
+ ]
180
+ }
181
+
164
182
  const countArg = (args: string[], flag: string, fallback: number) => {
165
183
  const at = args.indexOf(flag)
166
184
  if (at === -1) return fallback
@@ -175,9 +193,10 @@ export const stats: Command = {
175
193
  details: [
176
194
  "Reads the usage the API returned for every request in your Claude Code",
177
195
  "sessions, subagents included, counting each response once. Cost is at",
178
- "API list prices. The eunoe columns come from its request log: the input",
179
- "tokens it kept out of the requests it handled, and what that saved at",
180
- "cache-read prices once the cache writes its folds caused are paid for.",
196
+ "API list prices. The eunoe columns come from its request log: the",
197
+ "compactions Claude Code would have run on the conversations it handled,",
198
+ "the input tokens it kept out, and what that saved at cache-read prices",
199
+ "once the cache writes its folds caused are paid for.",
181
200
  ],
182
201
  options: [
183
202
  ["--days <n>", `Days to show (default ${DEFAULT_DAYS})`],
@@ -192,7 +211,9 @@ export const stats: Command = {
192
211
  const since = list[0].start
193
212
  const configDirs = claudeConfigDirs(readConfig())
194
213
  const usage = readUsage(usageFiles(configDirs, since), since)
195
- const logged = recentRequests(Number.POSITIVE_INFINITY).filter((record) => Date.parse(record.ts) >= since)
214
+ const all = recentRequests(Number.POSITIVE_INFINITY)
215
+ const avoided = nativeCompactions(all)
216
+ const logged = all.filter((record) => Date.parse(record.ts) >= since)
196
217
  for (const entry of usage) place(list, entry.at, (bucket) => bucket.usage.push(entry))
197
218
  const matched = matchLogged(usage, logged)
198
219
  for (const record of logged) place(list, Date.parse(record.ts), (bucket) => bucket.logged.push(record))
@@ -201,11 +222,20 @@ export const stats: Command = {
201
222
  line(` ${c.bold("eunoe stats")} ${c.dim(`the last ${count} ${weekly ? "weeks" : "days"} of Claude Code, from your session files`)}`)
202
223
  line()
203
224
  table([
204
- [weekly ? "Week" : "Day", "Requests", "Input", "Output", "Cache write", "Cache read", "API cost", "Not sent by eunoe", "Cost saved"].map((title) => c.dim(title)),
205
- ...list.map((bucket) => row(bucket.label, bucket.usage, bucket.logged, matched)),
206
- row("Total", usage, logged, matched, true),
225
+ [weekly ? "Week" : "Day", "Requests", "Input", "Output", "Cache write", "Cache read", "API cost", "Compactions avoided", "Tokens saved", "Cost saved"].map((title) => c.dim(title)),
226
+ ...list.map((bucket) => row(bucket.label, bucket.usage, bucket.logged, matched, avoided)),
227
+ row("Total", usage, logged, matched, avoided, true),
207
228
  ])
208
229
 
230
+ const longest = threadPeaks(all, avoided)
231
+ .filter((peak) => Date.parse(peak.lastSeen) >= since)
232
+ .slice(0, LONGEST)
233
+ if (longest.length > 0) {
234
+ line()
235
+ line(` ${c.dim("Longest conversations, by the history Claude Code would have had to carry")}`)
236
+ table(longest.map(longestRow))
237
+ }
238
+
209
239
  const models = byModel(usage)
210
240
  if (models.length > 0) {
211
241
  line()
@@ -216,7 +246,8 @@ export const stats: Command = {
216
246
  const handled = savings(logged, matched)
217
247
  const notes: Array<[label: string, text: string]> = [
218
248
  ["API cost", "At list prices, including cache writes and fast mode."],
219
- ["Not sent", "Input eunoe kept out, mostly history Claude Code would have read from cache."],
249
+ ["Compactions", `Times Claude Code would have compacted a conversation eunoe handled, at ${Math.round(REPLAY_NATIVE_COMPACT_AT * 100)}% of its window. eunoe kept every turn instead.`],
250
+ ["Tokens saved", "Input eunoe kept out, mostly history Claude Code would have read from cache."],
220
251
  ["Cost saved", "Those tokens at cache-read prices, minus what each fold cost to rewrite."],
221
252
  ...(handled.priced < handled.handled ? [["Not priced", `${(handled.handled - handled.priced).toLocaleString()} of ${handled.handled.toLocaleString()} eunoe requests: ${leftOut(handled)}.`] as [string, string]] : []),
222
253
  ...(summarize(usage).unpriced ? [["* No price", "Some models have no known price, so their tokens count but their cost doesn't."] as [string, string]] : []),
@@ -1,3 +1,5 @@
1
+ import { DEFAULT_RATIO } from "../../proxy/estimate"
2
+ import { NATIVE_SUMMARY_CHARS, REPLAY_NATIVE_COMPACT_AT } from "../replay"
1
3
  import type { RequestRecord } from "./inspect"
2
4
 
3
5
  export interface SessionStats {
@@ -84,3 +86,49 @@ export function savedShare(context: number, plain: number | null): number | null
84
86
  if (!plain) return null
85
87
  return plain > context ? 1 - context / plain : 0
86
88
  }
89
+
90
+ export function nativeCompactions(records: RequestRecord[]): Set<RequestRecord> {
91
+ const summary = Math.round(NATIVE_SUMMARY_CHARS * DEFAULT_RATIO)
92
+ const offsets = new Map<string, number>()
93
+ const compacted = new Set<RequestRecord>()
94
+ for (const record of records) {
95
+ const plain = scaled(record, record.charsIn)
96
+ const window = typeof record.window === "number" ? record.window : 0
97
+ if (typeof record.thread !== "string" || plain === null || window <= 0) continue
98
+ if (plain - (offsets.get(record.thread) ?? 0) <= REPLAY_NATIVE_COMPACT_AT * window) continue
99
+ compacted.add(record)
100
+ offsets.set(record.thread, plain - (scaled(record, record.charsFixed) ?? 0) - summary)
101
+ }
102
+ return compacted
103
+ }
104
+
105
+ export interface ThreadPeak {
106
+ thread: string
107
+ session: string
108
+ plain: number
109
+ sent: number
110
+ turns: number | null
111
+ compactions: number
112
+ lastSeen: string
113
+ }
114
+
115
+ export function threadPeaks(records: RequestRecord[], compacted: Set<RequestRecord>): ThreadPeak[] {
116
+ const peaks = new Map<string, ThreadPeak>()
117
+ for (const record of records) {
118
+ const plain = scaled(record, record.charsIn)
119
+ const session = sessionOf(record)
120
+ if (typeof record.thread !== "string" || !session || plain === null) continue
121
+ const previous = peaks.get(record.thread)
122
+ const higher = !previous || plain >= previous.plain
123
+ peaks.set(record.thread, {
124
+ thread: record.thread,
125
+ session,
126
+ plain: higher ? plain : previous.plain,
127
+ sent: higher ? inputOf(record) : previous.sent,
128
+ turns: record.turns ?? previous?.turns ?? null,
129
+ compactions: (previous?.compactions ?? 0) + (compacted.has(record) ? 1 : 0),
130
+ lastSeen: record.ts,
131
+ })
132
+ }
133
+ return [...peaks.values()].sort((a, b) => b.plain - a.plain)
134
+ }
@@ -43,7 +43,7 @@ export interface ReplayFailure {
43
43
 
44
44
  export const REPLAY_NATIVE_COMPACT_AT = 0.92
45
45
 
46
- const NATIVE_SUMMARY_CHARS = 30_000
46
+ export const NATIVE_SUMMARY_CHARS = 30_000
47
47
  const CACHE_READ_WEIGHT = 0.1
48
48
  const CACHE_WRITE_WEIGHT = 1.25
49
49
 
@@ -85,7 +85,7 @@ export function replaySession(messages: Message[], { config, window, fixedTokens
85
85
 
86
86
  const state: ThreadState = {}
87
87
  const eunoe = { sent: [] as Message[], sum: 0, count: 0, upTo: 0, recall: 0, compactedTurn: -2, previous: 0 }
88
- const plain = { sum: 0, count: 0, since: 0, previous: 0 }
88
+ const plain = { sum: 0, count: 0, since: 0, previous: 0, whole: 0, wholeCount: 0 }
89
89
 
90
90
  const sendEunoe = (i: number) => {
91
91
  const turn = turnAt[i - 1]
@@ -119,10 +119,12 @@ export function replaySession(messages: Message[], { config, window, fixedTokens
119
119
  for (; plain.since < i; plain.since += 1) {
120
120
  plain.sum += sizes[plain.since]
121
121
  plain.count += 1
122
+ plain.whole += sizes[plain.since]
123
+ plain.wholeCount += 1
122
124
  }
125
+ out.peakPlain = Math.max(out.peakPlain, tokens(fixedChars + listChars(plain.whole, plain.wholeCount)))
123
126
  let chars = fixedChars + listChars(plain.sum, plain.count)
124
127
  let cacheable = plain.previous
125
- out.peakPlain = Math.max(out.peakPlain, tokens(chars))
126
128
  if (tokens(chars) > REPLAY_NATIVE_COMPACT_AT * window) {
127
129
  out.nativeCompactions += 1
128
130
  out.discarded += tokens(chars - fixedChars)