tledger 0.2.1 → 0.3.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/README.md +152 -136
- package/bin/token-ledger-cache-image.mjs +1150 -0
- package/bin/token-ledger-rates.mjs +4 -1
- package/bin/token-ledger-terminal.mjs +89 -29
- package/bin/token-ledger-trend-image.mjs +1669 -587
- package/bin/token-ledger-trend-terminal.mjs +73 -28
- package/bin/token-ledger-trend.mjs +86 -26
- package/bin/token-ledger-tui.mjs +7 -3
- package/bin/token-ledger.mjs +333 -96
- package/docs/token-ledger-cli-week.png +0 -0
- package/docs/token-ledger-report-7-day.png +0 -0
- package/lib/token-ledger-importer.mjs +605 -282
- package/lib/token-ledger-snapshot.mjs +267 -0
- package/lib/token-ledger-usage.mjs +524 -0
- package/package.json +10 -5
|
@@ -11,22 +11,18 @@
|
|
|
11
11
|
* Node 22.13 or newer is required for node:sqlite.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { createHash
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
15
|
import { createReadStream } from "node:fs";
|
|
16
16
|
import {
|
|
17
17
|
access,
|
|
18
|
-
|
|
19
|
-
mkdir,
|
|
18
|
+
mkdtemp,
|
|
20
19
|
readdir,
|
|
21
|
-
rename,
|
|
22
20
|
rm,
|
|
23
21
|
stat,
|
|
24
|
-
writeFile,
|
|
25
22
|
} from "node:fs/promises";
|
|
26
|
-
import { homedir } from "node:os";
|
|
23
|
+
import { homedir, tmpdir } from "node:os";
|
|
27
24
|
import {
|
|
28
25
|
basename,
|
|
29
|
-
dirname,
|
|
30
26
|
extname,
|
|
31
27
|
resolve,
|
|
32
28
|
} from "node:path";
|
|
@@ -34,7 +30,13 @@ import { pathToFileURL } from "node:url";
|
|
|
34
30
|
import { createInterface } from "node:readline";
|
|
35
31
|
import { DatabaseSync } from "node:sqlite";
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
import { writePrivateSnapshot } from "./token-ledger-snapshot.mjs";
|
|
34
|
+
import {
|
|
35
|
+
buildUsageBuckets,
|
|
36
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
37
|
+
usageBucketStats,
|
|
38
|
+
} from "./token-ledger-usage.mjs";
|
|
39
|
+
|
|
38
40
|
const RATE_CARD_AS_OF = "2026-08-17";
|
|
39
41
|
const FAST_MODE_MULTIPLIER = 1.5;
|
|
40
42
|
const RATE_CARD_URL = "https://help.openai.com/en/articles/20001106";
|
|
@@ -42,6 +44,262 @@ const WEEK_MINUTES = 10_080;
|
|
|
42
44
|
const UUID_AT_END =
|
|
43
45
|
/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
|
|
44
46
|
|
|
47
|
+
function spoolText(value, maximumLength = 2_000) {
|
|
48
|
+
try {
|
|
49
|
+
return String(value ?? "").slice(0, maximumLength);
|
|
50
|
+
} catch {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function spoolSource(value) {
|
|
56
|
+
const descriptor = sourceDescriptor(value);
|
|
57
|
+
if (descriptor.subagent) return '{"subagent":true}';
|
|
58
|
+
return descriptor.label || "";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function createUsageSpool() {
|
|
62
|
+
const directory = await mkdtemp(resolve(tmpdir(), "token-ledger-import-"));
|
|
63
|
+
const path = resolve(directory, "usage.sqlite");
|
|
64
|
+
const database = new DatabaseSync(path);
|
|
65
|
+
database.exec(`
|
|
66
|
+
PRAGMA journal_mode = OFF;
|
|
67
|
+
PRAGMA synchronous = OFF;
|
|
68
|
+
PRAGMA temp_store = FILE;
|
|
69
|
+
CREATE TABLE token_events (
|
|
70
|
+
event_key TEXT PRIMARY KEY,
|
|
71
|
+
turn_id TEXT NOT NULL,
|
|
72
|
+
input_tokens REAL NOT NULL,
|
|
73
|
+
cached_input_tokens REAL NOT NULL,
|
|
74
|
+
cache_write_input_tokens REAL NOT NULL,
|
|
75
|
+
output_tokens REAL NOT NULL,
|
|
76
|
+
reasoning_tokens REAL NOT NULL,
|
|
77
|
+
total_tokens REAL NOT NULL,
|
|
78
|
+
timestamp TEXT NOT NULL,
|
|
79
|
+
thread_id TEXT NOT NULL,
|
|
80
|
+
model TEXT NOT NULL,
|
|
81
|
+
effort TEXT NOT NULL,
|
|
82
|
+
cwd TEXT NOT NULL,
|
|
83
|
+
git_origin TEXT,
|
|
84
|
+
raw_source TEXT,
|
|
85
|
+
service_tier TEXT,
|
|
86
|
+
original_likely INTEGER NOT NULL
|
|
87
|
+
) WITHOUT ROWID;
|
|
88
|
+
CREATE TABLE tool_calls (
|
|
89
|
+
call_key TEXT PRIMARY KEY,
|
|
90
|
+
turn_id TEXT NOT NULL,
|
|
91
|
+
thread_id TEXT NOT NULL,
|
|
92
|
+
original_likely INTEGER NOT NULL
|
|
93
|
+
) WITHOUT ROWID;
|
|
94
|
+
CREATE TABLE turn_origins (
|
|
95
|
+
turn_id TEXT PRIMARY KEY,
|
|
96
|
+
thread_id TEXT NOT NULL,
|
|
97
|
+
timestamp TEXT NOT NULL,
|
|
98
|
+
delta_ms REAL NOT NULL,
|
|
99
|
+
model TEXT NOT NULL,
|
|
100
|
+
effort TEXT NOT NULL,
|
|
101
|
+
cwd TEXT NOT NULL,
|
|
102
|
+
git_origin TEXT,
|
|
103
|
+
raw_source TEXT,
|
|
104
|
+
service_tier TEXT
|
|
105
|
+
) WITHOUT ROWID;
|
|
106
|
+
BEGIN IMMEDIATE;
|
|
107
|
+
`);
|
|
108
|
+
const insertToken = database.prepare(`
|
|
109
|
+
INSERT OR IGNORE INTO token_events (
|
|
110
|
+
event_key, turn_id, input_tokens, cached_input_tokens,
|
|
111
|
+
cache_write_input_tokens, output_tokens, reasoning_tokens,
|
|
112
|
+
total_tokens, timestamp, thread_id, model, effort, cwd,
|
|
113
|
+
git_origin, raw_source, service_tier, original_likely
|
|
114
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
115
|
+
`);
|
|
116
|
+
const promoteToken = database.prepare(`
|
|
117
|
+
UPDATE token_events
|
|
118
|
+
SET turn_id = ?, timestamp = ?, thread_id = ?, model = ?, effort = ?,
|
|
119
|
+
cwd = ?, git_origin = ?, raw_source = ?, service_tier = ?,
|
|
120
|
+
original_likely = 1
|
|
121
|
+
WHERE event_key = ? AND original_likely = 0
|
|
122
|
+
`);
|
|
123
|
+
const insertCall = database.prepare(`
|
|
124
|
+
INSERT OR IGNORE INTO tool_calls (
|
|
125
|
+
call_key, turn_id, thread_id, original_likely
|
|
126
|
+
) VALUES (?, ?, ?, ?)
|
|
127
|
+
`);
|
|
128
|
+
const promoteCall = database.prepare(`
|
|
129
|
+
UPDATE tool_calls
|
|
130
|
+
SET turn_id = ?, thread_id = ?, original_likely = 1
|
|
131
|
+
WHERE call_key = ? AND original_likely = 0
|
|
132
|
+
`);
|
|
133
|
+
const insertOrigin = database.prepare(`
|
|
134
|
+
INSERT INTO turn_origins (
|
|
135
|
+
turn_id, thread_id, timestamp, delta_ms, model, effort, cwd,
|
|
136
|
+
git_origin, raw_source, service_tier
|
|
137
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
138
|
+
ON CONFLICT(turn_id) DO UPDATE SET
|
|
139
|
+
thread_id = excluded.thread_id,
|
|
140
|
+
timestamp = excluded.timestamp,
|
|
141
|
+
delta_ms = excluded.delta_ms,
|
|
142
|
+
model = excluded.model,
|
|
143
|
+
effort = excluded.effort,
|
|
144
|
+
cwd = excluded.cwd,
|
|
145
|
+
git_origin = excluded.git_origin,
|
|
146
|
+
raw_source = excluded.raw_source,
|
|
147
|
+
service_tier = excluded.service_tier
|
|
148
|
+
WHERE excluded.delta_ms < turn_origins.delta_ms
|
|
149
|
+
`);
|
|
150
|
+
const updateOrigin = database.prepare(`
|
|
151
|
+
UPDATE turn_origins
|
|
152
|
+
SET thread_id = ?, timestamp = ?, model = ?, effort = ?, cwd = ?,
|
|
153
|
+
git_origin = ?, raw_source = ?, service_tier = ?
|
|
154
|
+
WHERE turn_id = ?
|
|
155
|
+
`);
|
|
156
|
+
let writing = true;
|
|
157
|
+
|
|
158
|
+
function originValues(candidate) {
|
|
159
|
+
return [
|
|
160
|
+
candidate.turnId,
|
|
161
|
+
candidate.threadId,
|
|
162
|
+
candidate.timestamp,
|
|
163
|
+
Number.isFinite(candidate.deltaMs)
|
|
164
|
+
? candidate.deltaMs
|
|
165
|
+
: Number.MAX_SAFE_INTEGER,
|
|
166
|
+
spoolText(candidate.model, 200),
|
|
167
|
+
spoolText(candidate.effort, 80),
|
|
168
|
+
spoolText(candidate.cwd),
|
|
169
|
+
spoolText(candidate.gitOrigin),
|
|
170
|
+
spoolSource(candidate.rawSource),
|
|
171
|
+
candidate.serviceTier == null
|
|
172
|
+
? null
|
|
173
|
+
: spoolText(candidate.serviceTier, 80),
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
directory,
|
|
179
|
+
insertOrigin(candidate) {
|
|
180
|
+
return insertOrigin.run(...originValues(candidate)).changes > 0;
|
|
181
|
+
},
|
|
182
|
+
updateOrigin(candidate) {
|
|
183
|
+
const values = originValues(candidate);
|
|
184
|
+
updateOrigin.run(
|
|
185
|
+
values[1],
|
|
186
|
+
values[2],
|
|
187
|
+
values[4],
|
|
188
|
+
values[5],
|
|
189
|
+
values[6],
|
|
190
|
+
values[7],
|
|
191
|
+
values[8],
|
|
192
|
+
values[9],
|
|
193
|
+
values[0],
|
|
194
|
+
);
|
|
195
|
+
},
|
|
196
|
+
insertToken(eventKey, turnId, usage, occurrence, originalLikely) {
|
|
197
|
+
const values = [
|
|
198
|
+
eventKey,
|
|
199
|
+
turnId,
|
|
200
|
+
usage.inputTokens,
|
|
201
|
+
usage.cachedInputTokens,
|
|
202
|
+
usage.cacheWriteInputTokens,
|
|
203
|
+
usage.outputTokens,
|
|
204
|
+
usage.reasoningTokens,
|
|
205
|
+
usage.totalTokens,
|
|
206
|
+
occurrence.timestamp,
|
|
207
|
+
occurrence.threadId,
|
|
208
|
+
spoolText(occurrence.model, 200),
|
|
209
|
+
spoolText(occurrence.effort, 80),
|
|
210
|
+
spoolText(occurrence.cwd),
|
|
211
|
+
spoolText(occurrence.gitOrigin),
|
|
212
|
+
spoolSource(occurrence.rawSource),
|
|
213
|
+
occurrence.serviceTier == null
|
|
214
|
+
? null
|
|
215
|
+
: spoolText(occurrence.serviceTier, 80),
|
|
216
|
+
originalLikely ? 1 : 0,
|
|
217
|
+
];
|
|
218
|
+
const inserted = insertToken.run(...values).changes > 0;
|
|
219
|
+
if (!inserted && originalLikely) {
|
|
220
|
+
promoteToken.run(
|
|
221
|
+
turnId,
|
|
222
|
+
occurrence.timestamp,
|
|
223
|
+
occurrence.threadId,
|
|
224
|
+
spoolText(occurrence.model, 200),
|
|
225
|
+
spoolText(occurrence.effort, 80),
|
|
226
|
+
spoolText(occurrence.cwd),
|
|
227
|
+
spoolText(occurrence.gitOrigin),
|
|
228
|
+
spoolSource(occurrence.rawSource),
|
|
229
|
+
occurrence.serviceTier == null
|
|
230
|
+
? null
|
|
231
|
+
: spoolText(occurrence.serviceTier, 80),
|
|
232
|
+
eventKey,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return inserted;
|
|
236
|
+
},
|
|
237
|
+
insertCall(callKey, turnId, threadId, originalLikely) {
|
|
238
|
+
const inserted = insertCall.run(
|
|
239
|
+
callKey,
|
|
240
|
+
turnId,
|
|
241
|
+
threadId,
|
|
242
|
+
originalLikely ? 1 : 0,
|
|
243
|
+
).changes > 0;
|
|
244
|
+
if (!inserted && originalLikely) {
|
|
245
|
+
promoteCall.run(turnId, threadId, callKey);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
finishWrites() {
|
|
249
|
+
if (!writing) return;
|
|
250
|
+
database.exec("COMMIT");
|
|
251
|
+
writing = false;
|
|
252
|
+
},
|
|
253
|
+
tokenRows() {
|
|
254
|
+
return database.prepare(`
|
|
255
|
+
SELECT token.event_key AS eventKey, token.turn_id AS turnId,
|
|
256
|
+
token.input_tokens AS inputTokens,
|
|
257
|
+
token.cached_input_tokens AS cachedInputTokens,
|
|
258
|
+
token.cache_write_input_tokens AS cacheWriteInputTokens,
|
|
259
|
+
token.output_tokens AS outputTokens,
|
|
260
|
+
token.reasoning_tokens AS reasoningTokens,
|
|
261
|
+
token.total_tokens AS totalTokens,
|
|
262
|
+
token.timestamp, token.thread_id AS threadId,
|
|
263
|
+
token.model, token.effort, token.cwd,
|
|
264
|
+
token.git_origin AS gitOrigin,
|
|
265
|
+
token.raw_source AS rawSource,
|
|
266
|
+
token.service_tier AS serviceTier,
|
|
267
|
+
token.original_likely AS originalLikely,
|
|
268
|
+
origin.thread_id AS originThreadId,
|
|
269
|
+
origin.timestamp AS originTimestamp,
|
|
270
|
+
origin.model AS originModel,
|
|
271
|
+
origin.effort AS originEffort,
|
|
272
|
+
origin.cwd AS originCwd,
|
|
273
|
+
origin.git_origin AS originGitOrigin,
|
|
274
|
+
origin.raw_source AS originRawSource,
|
|
275
|
+
origin.service_tier AS originServiceTier
|
|
276
|
+
FROM token_events AS token
|
|
277
|
+
LEFT JOIN turn_origins AS origin ON origin.turn_id = token.turn_id
|
|
278
|
+
ORDER BY token.timestamp, token.event_key
|
|
279
|
+
`).iterate();
|
|
280
|
+
},
|
|
281
|
+
callRows() {
|
|
282
|
+
return database.prepare(`
|
|
283
|
+
SELECT call.turn_id AS turnId,
|
|
284
|
+
COALESCE(origin.thread_id, call.thread_id) AS threadId
|
|
285
|
+
FROM tool_calls AS call
|
|
286
|
+
LEFT JOIN turn_origins AS origin ON origin.turn_id = call.turn_id
|
|
287
|
+
`).iterate();
|
|
288
|
+
},
|
|
289
|
+
close() {
|
|
290
|
+
if (writing) {
|
|
291
|
+
try {
|
|
292
|
+
database.exec("ROLLBACK");
|
|
293
|
+
} catch {
|
|
294
|
+
// The spool is disposable; cleanup below remains authoritative.
|
|
295
|
+
}
|
|
296
|
+
writing = false;
|
|
297
|
+
}
|
|
298
|
+
database.close();
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
45
303
|
const RATE_CARD = {
|
|
46
304
|
"gpt-5.6-sol": { input: 125, cached: 12.5, output: 750 },
|
|
47
305
|
"gpt-5.6-terra": { input: 50, cached: 5, output: 300 },
|
|
@@ -61,7 +319,7 @@ Usage:
|
|
|
61
319
|
node token-ledger-importer.mjs [options]
|
|
62
320
|
|
|
63
321
|
Options:
|
|
64
|
-
--output <file> Snapshot destination (default: token-ledger-snapshot.json)
|
|
322
|
+
--output <file> Snapshot destination (default: token-ledger-snapshot-v2.json.gz)
|
|
65
323
|
--codex-home <dir> Codex data root (default: CODEX_HOME or ~/.codex)
|
|
66
324
|
--since <ISO date> Ignore model calls before this timestamp
|
|
67
325
|
--no-archived Skip archived_sessions
|
|
@@ -75,7 +333,7 @@ text.`;
|
|
|
75
333
|
|
|
76
334
|
function parseArgs(argv) {
|
|
77
335
|
const options = {
|
|
78
|
-
output: resolve("token-ledger-snapshot.json"),
|
|
336
|
+
output: resolve("token-ledger-snapshot-v2.json.gz"),
|
|
79
337
|
codexHome: resolve(process.env.CODEX_HOME || `${homedir()}/.codex`),
|
|
80
338
|
includeArchived: true,
|
|
81
339
|
since: null,
|
|
@@ -131,24 +389,39 @@ function safeIso(value, fallback = null) {
|
|
|
131
389
|
}
|
|
132
390
|
|
|
133
391
|
function tokenTuple(value) {
|
|
134
|
-
|
|
392
|
+
// token_count payloads arrive from untrusted JSONL. Read the named usage
|
|
393
|
+
// fields once and coerce each to a finite count so malformed records
|
|
394
|
+
// contribute zeros instead of leaking odd shapes downstream.
|
|
395
|
+
const {
|
|
396
|
+
input_tokens: inputTokens,
|
|
397
|
+
cached_input_tokens: cachedInputTokens,
|
|
398
|
+
cache_write_input_tokens: cacheWriteInputTokens,
|
|
399
|
+
output_tokens: outputTokens,
|
|
400
|
+
reasoning_output_tokens: reasoningOutputTokens,
|
|
401
|
+
total_tokens: totalTokens,
|
|
402
|
+
} = value ?? {};
|
|
135
403
|
return [
|
|
136
|
-
asFiniteNumber(
|
|
137
|
-
asFiniteNumber(
|
|
138
|
-
asFiniteNumber(
|
|
139
|
-
asFiniteNumber(
|
|
140
|
-
asFiniteNumber(
|
|
141
|
-
asFiniteNumber(
|
|
404
|
+
asFiniteNumber(inputTokens),
|
|
405
|
+
asFiniteNumber(cachedInputTokens),
|
|
406
|
+
asFiniteNumber(cacheWriteInputTokens),
|
|
407
|
+
asFiniteNumber(outputTokens),
|
|
408
|
+
asFiniteNumber(reasoningOutputTokens),
|
|
409
|
+
asFiniteNumber(totalTokens),
|
|
142
410
|
];
|
|
143
411
|
}
|
|
144
412
|
|
|
145
413
|
function usageFromTuple(tuple) {
|
|
414
|
+
// Cached input is a subset of input and reasoning is a subset of output.
|
|
415
|
+
// Clamp at export so one out-of-range source record cannot skew subset
|
|
416
|
+
// math downstream.
|
|
417
|
+
const inputTokens = tuple[0];
|
|
418
|
+
const outputTokens = tuple[3];
|
|
146
419
|
return {
|
|
147
|
-
inputTokens
|
|
148
|
-
cachedInputTokens: tuple[1],
|
|
420
|
+
inputTokens,
|
|
421
|
+
cachedInputTokens: Math.min(inputTokens, tuple[1]),
|
|
149
422
|
cacheWriteInputTokens: tuple[2],
|
|
150
|
-
outputTokens
|
|
151
|
-
reasoningTokens: tuple[4],
|
|
423
|
+
outputTokens,
|
|
424
|
+
reasoningTokens: Math.min(outputTokens, tuple[4]),
|
|
152
425
|
totalTokens: tuple[5],
|
|
153
426
|
};
|
|
154
427
|
}
|
|
@@ -162,10 +435,14 @@ function hasDetailedBreakdown(usage) {
|
|
|
162
435
|
}
|
|
163
436
|
|
|
164
437
|
function normalizeModel(model) {
|
|
438
|
+
// Collapse underscore and whitespace separators to dashes so variants like
|
|
439
|
+
// "gpt-5.4 mini" resolve to their own rate-card entry instead of falling
|
|
440
|
+
// back to the base model's higher rate. Keep in lockstep with
|
|
441
|
+
// normalizeModel in bin/token-ledger-rates.mjs.
|
|
165
442
|
const value = String(model || "unknown")
|
|
166
443
|
.trim()
|
|
167
444
|
.toLowerCase()
|
|
168
|
-
.
|
|
445
|
+
.replace(/[\s_]+/g, "-");
|
|
169
446
|
if (RATE_CARD[value]) return value;
|
|
170
447
|
if (value.startsWith("gpt-5.6-sol")) return "gpt-5.6-sol";
|
|
171
448
|
if (value.startsWith("gpt-5.6-terra")) return "gpt-5.6-terra";
|
|
@@ -193,25 +470,40 @@ function creditsForUsage(model, usage) {
|
|
|
193
470
|
);
|
|
194
471
|
}
|
|
195
472
|
|
|
196
|
-
function
|
|
197
|
-
if (!value) return null;
|
|
198
|
-
if (typeof value === "object") return value;
|
|
199
|
-
if (typeof value !== "string") return null;
|
|
200
|
-
const trimmed = value.trim();
|
|
201
|
-
if (!trimmed.startsWith("{")) return trimmed;
|
|
473
|
+
function primitiveString(value) {
|
|
202
474
|
try {
|
|
203
|
-
|
|
475
|
+
const text = String.prototype.valueOf.call(value);
|
|
476
|
+
return text === value ? text : null;
|
|
204
477
|
} catch {
|
|
205
|
-
return
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function sourceDescriptor(value) {
|
|
483
|
+
// Thread sources reach us in two representations: rollout session_meta
|
|
484
|
+
// lines carry parsed JSON (a label string or an object with subagent
|
|
485
|
+
// details) while the sqlite source column carries the same value as text.
|
|
486
|
+
// Parse both here, once, so callers only branch on the named fields.
|
|
487
|
+
if (value == null) return { label: null, subagent: null };
|
|
488
|
+
const sourceText = primitiveString(value);
|
|
489
|
+
if (sourceText === null) {
|
|
490
|
+
return { label: null, subagent: value.subagent || null };
|
|
491
|
+
}
|
|
492
|
+
const text = sourceText.trim();
|
|
493
|
+
if (text.startsWith("{")) {
|
|
494
|
+
try {
|
|
495
|
+
const parsed = JSON.parse(text);
|
|
496
|
+
return { label: null, subagent: parsed.subagent || null };
|
|
497
|
+
} catch {
|
|
498
|
+
// Not JSON after all; fall through to the plain-label case.
|
|
499
|
+
}
|
|
206
500
|
}
|
|
501
|
+
return { label: text || null, subagent: null };
|
|
207
502
|
}
|
|
208
503
|
|
|
209
504
|
function sourceLabels(threadSource, rawSource) {
|
|
210
|
-
const source =
|
|
211
|
-
if (
|
|
212
|
-
threadSource === "subagent" ||
|
|
213
|
-
(source && typeof source === "object" && source.subagent)
|
|
214
|
-
) {
|
|
505
|
+
const source = sourceDescriptor(rawSource);
|
|
506
|
+
if (threadSource === "subagent" || source.subagent) {
|
|
215
507
|
return { source: "subagent", useType: "subagent" };
|
|
216
508
|
}
|
|
217
509
|
if (threadSource === "automation") {
|
|
@@ -220,10 +512,15 @@ function sourceLabels(threadSource, rawSource) {
|
|
|
220
512
|
if (threadSource === "realtime_voice") {
|
|
221
513
|
return { source: "voice", useType: "voice" };
|
|
222
514
|
}
|
|
223
|
-
if (source === "exec") return { source: "cli", useType: "cli" };
|
|
224
|
-
if (source === "vscode")
|
|
225
|
-
|
|
226
|
-
|
|
515
|
+
if (source.label === "exec") return { source: "cli", useType: "cli" };
|
|
516
|
+
if (source.label === "vscode") {
|
|
517
|
+
return { source: "desktop", useType: "interactive" };
|
|
518
|
+
}
|
|
519
|
+
if (source.label) {
|
|
520
|
+
return {
|
|
521
|
+
source: source.label.slice(0, 40),
|
|
522
|
+
useType: threadSource || "interactive",
|
|
523
|
+
};
|
|
227
524
|
}
|
|
228
525
|
return { source: "unknown", useType: threadSource || "unknown" };
|
|
229
526
|
}
|
|
@@ -291,28 +588,6 @@ async function pathExists(path) {
|
|
|
291
588
|
}
|
|
292
589
|
}
|
|
293
590
|
|
|
294
|
-
export async function writePrivateSnapshot(output, snapshot) {
|
|
295
|
-
const destination = resolve(output);
|
|
296
|
-
const directory = dirname(destination);
|
|
297
|
-
const temporary = resolve(
|
|
298
|
-
directory,
|
|
299
|
-
`.token-ledger-${process.pid}-${randomUUID()}.tmp`,
|
|
300
|
-
);
|
|
301
|
-
await mkdir(directory, { recursive: true });
|
|
302
|
-
try {
|
|
303
|
-
await writeFile(temporary, `${JSON.stringify(snapshot)}\n`, {
|
|
304
|
-
encoding: "utf8",
|
|
305
|
-
flag: "wx",
|
|
306
|
-
mode: 0o600,
|
|
307
|
-
});
|
|
308
|
-
await chmod(temporary, 0o600);
|
|
309
|
-
await rename(temporary, destination);
|
|
310
|
-
await chmod(destination, 0o600);
|
|
311
|
-
} finally {
|
|
312
|
-
await rm(temporary, { force: true });
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
591
|
export async function listJsonlFiles(root) {
|
|
317
592
|
if (!(await pathExists(root))) return [];
|
|
318
593
|
const found = [];
|
|
@@ -440,14 +715,22 @@ function taskStartCandidate(record, threadId, stateRow, fileContext) {
|
|
|
440
715
|
}
|
|
441
716
|
|
|
442
717
|
function rememberQuota(quotaMap, rateLimits, occurrence) {
|
|
443
|
-
|
|
444
|
-
|
|
718
|
+
// rate_limits payloads come straight from JSONL. Read the named fields
|
|
719
|
+
// once; records without usable buckets drop out via the filter below.
|
|
720
|
+
const {
|
|
721
|
+
primary,
|
|
722
|
+
secondary,
|
|
723
|
+
limit_id: limitId,
|
|
724
|
+
limit_name: limitName,
|
|
725
|
+
plan_type: planType,
|
|
726
|
+
} = rateLimits ?? {};
|
|
727
|
+
const buckets = [primary, secondary].filter(Boolean);
|
|
445
728
|
for (const bucket of buckets) {
|
|
446
729
|
const windowMinutes = asFiniteNumber(bucket.window_minutes);
|
|
447
730
|
const usedPercent = asFiniteNumber(bucket.used_percent);
|
|
448
731
|
const resetsAt = asFiniteNumber(bucket.resets_at);
|
|
449
732
|
if (!windowMinutes || !resetsAt) continue;
|
|
450
|
-
const limitKey = String(
|
|
733
|
+
const limitKey = String(limitId || limitName || "anonymous");
|
|
451
734
|
const key = [
|
|
452
735
|
hash(limitKey, 16),
|
|
453
736
|
windowMinutes,
|
|
@@ -456,15 +739,16 @@ function rememberQuota(quotaMap, rateLimits, occurrence) {
|
|
|
456
739
|
].join("|");
|
|
457
740
|
const candidate = {
|
|
458
741
|
id: `quota-${hash(key)}`,
|
|
742
|
+
// Keep the first and last occurrence of an unchanged reading without
|
|
743
|
+
// storing every repeated provider sample in the snapshot.
|
|
459
744
|
timestamp: occurrence.timestamp,
|
|
745
|
+
lastSeenAt: occurrence.timestamp,
|
|
460
746
|
usedPercent,
|
|
461
747
|
windowMinutes,
|
|
462
748
|
resetsAt,
|
|
463
|
-
planType: String(
|
|
749
|
+
planType: String(planType || "unknown"),
|
|
464
750
|
limitKey: hash(limitKey, 16),
|
|
465
|
-
limitName:
|
|
466
|
-
? String(rateLimits.limit_name).slice(0, 80)
|
|
467
|
-
: null,
|
|
751
|
+
limitName: limitName ? String(limitName).slice(0, 80) : null,
|
|
468
752
|
source: "log",
|
|
469
753
|
turnId: occurrence.turnId || null,
|
|
470
754
|
originalLikely: occurrence.originalLikely,
|
|
@@ -472,11 +756,21 @@ function rememberQuota(quotaMap, rateLimits, occurrence) {
|
|
|
472
756
|
const current = quotaMap.get(key);
|
|
473
757
|
if (
|
|
474
758
|
!current ||
|
|
475
|
-
(!current.originalLikely && candidate.originalLikely)
|
|
476
|
-
(current.originalLikely === candidate.originalLikely &&
|
|
477
|
-
candidate.timestamp < current.timestamp)
|
|
759
|
+
(!current.originalLikely && candidate.originalLikely)
|
|
478
760
|
) {
|
|
479
761
|
quotaMap.set(key, candidate);
|
|
762
|
+
} else if (current.originalLikely === candidate.originalLikely) {
|
|
763
|
+
quotaMap.set(key, {
|
|
764
|
+
...current,
|
|
765
|
+
timestamp:
|
|
766
|
+
candidate.timestamp < current.timestamp
|
|
767
|
+
? candidate.timestamp
|
|
768
|
+
: current.timestamp,
|
|
769
|
+
lastSeenAt:
|
|
770
|
+
candidate.timestamp > current.lastSeenAt
|
|
771
|
+
? candidate.timestamp
|
|
772
|
+
: current.lastSeenAt,
|
|
773
|
+
});
|
|
480
774
|
}
|
|
481
775
|
}
|
|
482
776
|
}
|
|
@@ -514,6 +808,7 @@ async function scanRollout(path, context) {
|
|
|
514
808
|
const callOrdinals = new Map();
|
|
515
809
|
let currentTurnId = "";
|
|
516
810
|
let currentCandidate = null;
|
|
811
|
+
let currentCandidateSelected = false;
|
|
517
812
|
let previousCumulative = null;
|
|
518
813
|
|
|
519
814
|
const input = createReadStream(path, { encoding: "utf8" });
|
|
@@ -557,10 +852,7 @@ async function scanRollout(path, context) {
|
|
|
557
852
|
stateRow,
|
|
558
853
|
fileContext,
|
|
559
854
|
);
|
|
560
|
-
|
|
561
|
-
if (!currentBest || currentCandidate.deltaMs < currentBest.deltaMs) {
|
|
562
|
-
context.origins.set(currentTurnId, currentCandidate);
|
|
563
|
-
}
|
|
855
|
+
currentCandidateSelected = context.spool.insertOrigin(currentCandidate);
|
|
564
856
|
}
|
|
565
857
|
continue;
|
|
566
858
|
}
|
|
@@ -574,6 +866,9 @@ async function scanRollout(path, context) {
|
|
|
574
866
|
currentCandidate.model = fileContext.model;
|
|
575
867
|
currentCandidate.effort = fileContext.effort;
|
|
576
868
|
currentCandidate.cwd = fileContext.cwd;
|
|
869
|
+
if (currentCandidateSelected) {
|
|
870
|
+
context.spool.updateOrigin(currentCandidate);
|
|
871
|
+
}
|
|
577
872
|
}
|
|
578
873
|
continue;
|
|
579
874
|
}
|
|
@@ -617,14 +912,12 @@ async function scanRollout(path, context) {
|
|
|
617
912
|
const callKey = call.stableId
|
|
618
913
|
? `id|${call.stableId}`
|
|
619
914
|
: `ordinal|${ordinalBase}|${ordinal}`;
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
});
|
|
627
|
-
}
|
|
915
|
+
context.spool.insertCall(
|
|
916
|
+
callKey,
|
|
917
|
+
currentTurnId,
|
|
918
|
+
threadId,
|
|
919
|
+
originalLikely,
|
|
920
|
+
);
|
|
628
921
|
continue;
|
|
629
922
|
}
|
|
630
923
|
|
|
@@ -657,24 +950,16 @@ async function scanRollout(path, context) {
|
|
|
657
950
|
}
|
|
658
951
|
previousCumulative = totalTuple[5];
|
|
659
952
|
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
existing.occurrence = occurrence;
|
|
665
|
-
existing.originalLikely = true;
|
|
666
|
-
}
|
|
667
|
-
continue;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
context.tokens.set(eventKey, {
|
|
671
|
-
key: eventKey,
|
|
672
|
-
turnId: currentTurnId,
|
|
673
|
-
usage: usageFromTuple(lastTuple),
|
|
953
|
+
const inserted = context.spool.insertToken(
|
|
954
|
+
eventKey,
|
|
955
|
+
currentTurnId,
|
|
956
|
+
usageFromTuple(lastTuple),
|
|
674
957
|
occurrence,
|
|
675
958
|
originalLikely,
|
|
676
|
-
|
|
677
|
-
|
|
959
|
+
);
|
|
960
|
+
if (!inserted) {
|
|
961
|
+
context.duplicateEventsSkipped += 1;
|
|
962
|
+
}
|
|
678
963
|
}
|
|
679
964
|
}
|
|
680
965
|
|
|
@@ -708,144 +993,183 @@ function threadMetadata(threadId, stateRows, titles, parents, fallback = {}) {
|
|
|
708
993
|
};
|
|
709
994
|
}
|
|
710
995
|
|
|
996
|
+
function resolvedOccurrence(token) {
|
|
997
|
+
const origin = token.originThreadId == null
|
|
998
|
+
? null
|
|
999
|
+
: {
|
|
1000
|
+
threadId: token.originThreadId,
|
|
1001
|
+
timestamp: token.originTimestamp,
|
|
1002
|
+
model: token.originModel,
|
|
1003
|
+
effort: token.originEffort,
|
|
1004
|
+
cwd: token.originCwd,
|
|
1005
|
+
gitOrigin: token.originGitOrigin,
|
|
1006
|
+
rawSource: token.originRawSource,
|
|
1007
|
+
serviceTier: token.originServiceTier,
|
|
1008
|
+
};
|
|
1009
|
+
const occurrence = token.originalLikely || !origin ? token : origin;
|
|
1010
|
+
return {
|
|
1011
|
+
origin,
|
|
1012
|
+
occurrence,
|
|
1013
|
+
threadId: origin?.threadId || occurrence.threadId,
|
|
1014
|
+
timestamp: token.timestamp || origin?.timestamp,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function newThreadAggregate(metadata, timestamp) {
|
|
1019
|
+
return {
|
|
1020
|
+
metadata,
|
|
1021
|
+
firstActiveAt: timestamp,
|
|
1022
|
+
lastActiveAt: timestamp,
|
|
1023
|
+
inputTokens: 0,
|
|
1024
|
+
cachedInputTokens: 0,
|
|
1025
|
+
outputTokens: 0,
|
|
1026
|
+
reasoningTokens: 0,
|
|
1027
|
+
totalTokens: 0,
|
|
1028
|
+
toolCalls: 0,
|
|
1029
|
+
detailedTokens: 0,
|
|
1030
|
+
unknownBreakdownTokens: 0,
|
|
1031
|
+
rateCardCredits: 0,
|
|
1032
|
+
ratedTokens: 0,
|
|
1033
|
+
eventCount: 0,
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function addToThreadAggregate(aggregate, event) {
|
|
1038
|
+
aggregate.lastActiveAt = event.timestamp;
|
|
1039
|
+
aggregate.inputTokens += event.inputTokens;
|
|
1040
|
+
aggregate.cachedInputTokens += event.cachedInputTokens;
|
|
1041
|
+
aggregate.outputTokens += event.outputTokens;
|
|
1042
|
+
aggregate.reasoningTokens += event.reasoningTokens;
|
|
1043
|
+
aggregate.totalTokens += event.totalTokens;
|
|
1044
|
+
aggregate.toolCalls += event.toolCalls;
|
|
1045
|
+
aggregate.eventCount += 1;
|
|
1046
|
+
if (event.breakdownAvailable) {
|
|
1047
|
+
aggregate.detailedTokens += event.totalTokens;
|
|
1048
|
+
} else {
|
|
1049
|
+
aggregate.unknownBreakdownTokens += event.totalTokens;
|
|
1050
|
+
}
|
|
1051
|
+
if (event.rateCardCredits !== null) {
|
|
1052
|
+
aggregate.rateCardCredits += event.rateCardCredits;
|
|
1053
|
+
aggregate.ratedTokens += event.totalTokens;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
711
1057
|
function buildSnapshot(context, options, titles) {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
titles,
|
|
728
|
-
context.parents,
|
|
729
|
-
origin || occurrence,
|
|
730
|
-
);
|
|
731
|
-
const timestamp = occurrence.timestamp || origin?.timestamp;
|
|
732
|
-
if (
|
|
733
|
-
options.since &&
|
|
734
|
-
new Date(timestamp).getTime() < options.since.getTime()
|
|
735
|
-
) {
|
|
736
|
-
continue;
|
|
737
|
-
}
|
|
738
|
-
const breakdownAvailable = hasDetailedBreakdown(token.usage);
|
|
739
|
-
const serviceTier = occurrence.serviceTier || null;
|
|
740
|
-
const baseCredits = creditsForUsage(metadata.model, token.usage);
|
|
741
|
-
events.push({
|
|
742
|
-
...token.usage,
|
|
743
|
-
id: `evt-${hash(token.key)}`,
|
|
744
|
-
timestamp,
|
|
745
|
-
threadId,
|
|
746
|
-
threadTitle: metadata.title,
|
|
747
|
-
project: metadata.project,
|
|
748
|
-
model: metadata.model,
|
|
749
|
-
effort: metadata.effort,
|
|
750
|
-
source: metadata.source,
|
|
751
|
-
useType: metadata.useType,
|
|
752
|
-
turnId: token.turnId || "",
|
|
753
|
-
toolCalls: 0,
|
|
754
|
-
serviceTier,
|
|
755
|
-
rateCardCredits:
|
|
756
|
-
baseCredits === null
|
|
757
|
-
? null
|
|
758
|
-
: serviceTier === "priority"
|
|
759
|
-
? baseCredits * FAST_MODE_MULTIPLIER
|
|
760
|
-
: baseCredits,
|
|
761
|
-
breakdownAvailable,
|
|
762
|
-
dedupeQuality: token.dedupeQuality,
|
|
763
|
-
});
|
|
1058
|
+
context.spool.finishWrites();
|
|
1059
|
+
const sinceMs = options.since ? options.since.getTime() : -Infinity;
|
|
1060
|
+
const lastEventKeyByTurn = new Map();
|
|
1061
|
+
let earliestEventAt = null;
|
|
1062
|
+
let latestEventAt = null;
|
|
1063
|
+
|
|
1064
|
+
// The spool is ordered on disk. This lightweight pass finds the event that
|
|
1065
|
+
// receives each turn's tool calls without retaining the usage rows.
|
|
1066
|
+
for (const token of context.spool.tokenRows()) {
|
|
1067
|
+
const { threadId, timestamp } = resolvedOccurrence(token);
|
|
1068
|
+
if (Date.parse(timestamp) < sinceMs) continue;
|
|
1069
|
+
earliestEventAt ||= timestamp;
|
|
1070
|
+
latestEventAt = timestamp;
|
|
1071
|
+
const key = token.turnId || `thread:${threadId}`;
|
|
1072
|
+
lastEventKeyByTurn.set(key, token.eventKey);
|
|
764
1073
|
}
|
|
765
|
-
events.sort(
|
|
766
|
-
(left, right) =>
|
|
767
|
-
new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime(),
|
|
768
|
-
);
|
|
769
1074
|
|
|
770
1075
|
const toolCounts = new Map();
|
|
771
|
-
for (const call of context.
|
|
772
|
-
const
|
|
773
|
-
const threadId = origin?.threadId || call.threadId;
|
|
774
|
-
const key = call.turnId || `thread:${threadId}`;
|
|
1076
|
+
for (const call of context.spool.callRows()) {
|
|
1077
|
+
const key = call.turnId || `thread:${call.threadId}`;
|
|
775
1078
|
toolCounts.set(key, (toolCounts.get(key) || 0) + 1);
|
|
776
1079
|
}
|
|
777
|
-
const lastEventByTurn = new Map();
|
|
778
|
-
for (const event of events) {
|
|
779
|
-
const key = event.turnId || `thread:${event.threadId}`;
|
|
780
|
-
lastEventByTurn.set(key, event);
|
|
781
|
-
}
|
|
782
|
-
for (const [key, count] of toolCounts) {
|
|
783
|
-
const event = lastEventByTurn.get(key);
|
|
784
|
-
if (event) event.toolCalls = count;
|
|
785
|
-
}
|
|
786
1080
|
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1081
|
+
const threadAggregates = new Map();
|
|
1082
|
+
let observedTokens = 0;
|
|
1083
|
+
let detailedTokens = 0;
|
|
1084
|
+
let legacyHeuristicEvents = 0;
|
|
1085
|
+
let observedModelCalls = 0;
|
|
1086
|
+
|
|
1087
|
+
function* compactableEvents() {
|
|
1088
|
+
for (const token of context.spool.tokenRows()) {
|
|
1089
|
+
const { origin, occurrence, threadId, timestamp } = resolvedOccurrence(token);
|
|
1090
|
+
if (Date.parse(timestamp) < sinceMs) continue;
|
|
1091
|
+
const metadata = threadMetadata(
|
|
1092
|
+
threadId,
|
|
1093
|
+
context.stateRows,
|
|
1094
|
+
titles,
|
|
1095
|
+
context.parents,
|
|
1096
|
+
origin || occurrence,
|
|
1097
|
+
);
|
|
1098
|
+
const usage = {
|
|
1099
|
+
inputTokens: Number(token.inputTokens),
|
|
1100
|
+
cachedInputTokens: Number(token.cachedInputTokens),
|
|
1101
|
+
cacheWriteInputTokens: Number(token.cacheWriteInputTokens),
|
|
1102
|
+
outputTokens: Number(token.outputTokens),
|
|
1103
|
+
reasoningTokens: Number(token.reasoningTokens),
|
|
1104
|
+
totalTokens: Number(token.totalTokens),
|
|
1105
|
+
};
|
|
1106
|
+
const breakdownAvailable = hasDetailedBreakdown(usage);
|
|
1107
|
+
const serviceTier = occurrence.serviceTier || null;
|
|
1108
|
+
const baseCredits = creditsForUsage(metadata.model, usage);
|
|
1109
|
+
const turnKey = token.turnId || `thread:${threadId}`;
|
|
1110
|
+
const event = {
|
|
1111
|
+
...usage,
|
|
1112
|
+
timestamp,
|
|
1113
|
+
threadId,
|
|
1114
|
+
project: metadata.project,
|
|
1115
|
+
model: metadata.model,
|
|
1116
|
+
effort: metadata.effort,
|
|
1117
|
+
source: metadata.source,
|
|
1118
|
+
useType: metadata.useType,
|
|
1119
|
+
toolCalls:
|
|
1120
|
+
lastEventKeyByTurn.get(turnKey) === token.eventKey
|
|
1121
|
+
? toolCounts.get(turnKey) || 0
|
|
1122
|
+
: 0,
|
|
1123
|
+
serviceTier,
|
|
1124
|
+
rateCardCredits:
|
|
1125
|
+
baseCredits === null
|
|
1126
|
+
? null
|
|
1127
|
+
: serviceTier === "priority"
|
|
1128
|
+
? baseCredits * FAST_MODE_MULTIPLIER
|
|
1129
|
+
: baseCredits,
|
|
1130
|
+
breakdownAvailable,
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
let aggregate = threadAggregates.get(threadId);
|
|
1134
|
+
if (!aggregate) {
|
|
1135
|
+
aggregate = newThreadAggregate(metadata, timestamp);
|
|
1136
|
+
threadAggregates.set(threadId, aggregate);
|
|
1137
|
+
}
|
|
1138
|
+
addToThreadAggregate(aggregate, event);
|
|
1139
|
+
observedTokens += event.totalTokens;
|
|
1140
|
+
if (breakdownAvailable) detailedTokens += event.totalTokens;
|
|
1141
|
+
if (!token.turnId) legacyHeuristicEvents += 1;
|
|
1142
|
+
observedModelCalls += 1;
|
|
1143
|
+
yield event;
|
|
1144
|
+
}
|
|
792
1145
|
}
|
|
793
1146
|
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1147
|
+
const usageBuckets = buildUsageBuckets(compactableEvents(), {
|
|
1148
|
+
latestTimestampMs: latestEventAt ? Date.parse(latestEventAt) : 0,
|
|
1149
|
+
});
|
|
1150
|
+
const usageStats = usageBucketStats(usageBuckets);
|
|
1151
|
+
toolCounts.clear();
|
|
1152
|
+
lastEventKeyByTurn.clear();
|
|
1153
|
+
|
|
1154
|
+
const allThreadIds = new Set(context.stateRows.keys());
|
|
1155
|
+
for (const threadId of threadAggregates.keys()) allThreadIds.add(threadId);
|
|
798
1156
|
const threads = [];
|
|
799
1157
|
for (const threadId of allThreadIds) {
|
|
800
|
-
const
|
|
801
|
-
const
|
|
802
|
-
const last = rows.at(-1);
|
|
803
|
-
const origin = first
|
|
804
|
-
? context.origins.get(first.turnId)
|
|
805
|
-
: null;
|
|
806
|
-
const metadata = threadMetadata(
|
|
1158
|
+
const aggregate = threadAggregates.get(threadId);
|
|
1159
|
+
const metadata = aggregate?.metadata || threadMetadata(
|
|
807
1160
|
threadId,
|
|
808
1161
|
context.stateRows,
|
|
809
1162
|
titles,
|
|
810
1163
|
context.parents,
|
|
811
|
-
origin || first || {},
|
|
812
|
-
);
|
|
813
|
-
const totals = rows.reduce(
|
|
814
|
-
(sum, event) => {
|
|
815
|
-
sum.inputTokens += event.inputTokens;
|
|
816
|
-
sum.cachedInputTokens += event.cachedInputTokens;
|
|
817
|
-
sum.outputTokens += event.outputTokens;
|
|
818
|
-
sum.reasoningTokens += event.reasoningTokens;
|
|
819
|
-
sum.totalTokens += event.totalTokens;
|
|
820
|
-
sum.toolCalls += event.toolCalls;
|
|
821
|
-
if (event.breakdownAvailable) sum.detailedTokens += event.totalTokens;
|
|
822
|
-
else sum.unknownBreakdownTokens += event.totalTokens;
|
|
823
|
-
if (event.rateCardCredits !== null) {
|
|
824
|
-
sum.rateCardCredits += event.rateCardCredits;
|
|
825
|
-
sum.ratedTokens += event.totalTokens;
|
|
826
|
-
}
|
|
827
|
-
return sum;
|
|
828
|
-
},
|
|
829
|
-
{
|
|
830
|
-
inputTokens: 0,
|
|
831
|
-
cachedInputTokens: 0,
|
|
832
|
-
outputTokens: 0,
|
|
833
|
-
reasoningTokens: 0,
|
|
834
|
-
totalTokens: 0,
|
|
835
|
-
toolCalls: 0,
|
|
836
|
-
detailedTokens: 0,
|
|
837
|
-
unknownBreakdownTokens: 0,
|
|
838
|
-
rateCardCredits: 0,
|
|
839
|
-
ratedTokens: 0,
|
|
840
|
-
},
|
|
841
1164
|
);
|
|
842
|
-
if (
|
|
843
|
-
const
|
|
844
|
-
|
|
1165
|
+
if (!aggregate && !(metadata.reportedCumulativeTokens > 0)) continue;
|
|
1166
|
+
const eventCount = aggregate?.eventCount || 0;
|
|
1167
|
+
const threadCoverage =
|
|
1168
|
+
eventCount === 0
|
|
845
1169
|
? "unresolved"
|
|
846
|
-
:
|
|
1170
|
+
: aggregate.detailedTokens === aggregate.totalTokens
|
|
847
1171
|
? "complete"
|
|
848
|
-
:
|
|
1172
|
+
: aggregate.detailedTokens > 0
|
|
849
1173
|
? "partial"
|
|
850
1174
|
: "total-only";
|
|
851
1175
|
threads.push({
|
|
@@ -857,27 +1181,32 @@ function buildSnapshot(context, options, titles) {
|
|
|
857
1181
|
source: metadata.source,
|
|
858
1182
|
useType: metadata.useType,
|
|
859
1183
|
parentThreadId: metadata.parentThreadId,
|
|
860
|
-
firstActiveAt:
|
|
861
|
-
lastActiveAt:
|
|
862
|
-
totalTokens:
|
|
863
|
-
detailedTokens:
|
|
864
|
-
unknownBreakdownTokens:
|
|
1184
|
+
firstActiveAt: aggregate?.firstActiveAt || metadata.createdAt,
|
|
1185
|
+
lastActiveAt: aggregate?.lastActiveAt || metadata.updatedAt,
|
|
1186
|
+
totalTokens: aggregate?.totalTokens || 0,
|
|
1187
|
+
detailedTokens: aggregate?.detailedTokens || 0,
|
|
1188
|
+
unknownBreakdownTokens: aggregate?.unknownBreakdownTokens || 0,
|
|
865
1189
|
reportedCumulativeTokens: metadata.reportedCumulativeTokens,
|
|
866
|
-
inputTokens:
|
|
867
|
-
cachedInputTokens:
|
|
868
|
-
outputTokens:
|
|
869
|
-
reasoningTokens:
|
|
1190
|
+
inputTokens: aggregate?.inputTokens || 0,
|
|
1191
|
+
cachedInputTokens: aggregate?.cachedInputTokens || 0,
|
|
1192
|
+
outputTokens: aggregate?.outputTokens || 0,
|
|
1193
|
+
reasoningTokens: aggregate?.reasoningTokens || 0,
|
|
870
1194
|
rateCardCredits:
|
|
871
|
-
|
|
872
|
-
|
|
1195
|
+
aggregate?.totalTokens > 0 &&
|
|
1196
|
+
aggregate.ratedTokens === aggregate.totalTokens
|
|
1197
|
+
? aggregate.rateCardCredits
|
|
873
1198
|
: null,
|
|
874
|
-
ratedTokens:
|
|
875
|
-
toolCalls:
|
|
876
|
-
eventCount
|
|
877
|
-
coverage,
|
|
1199
|
+
ratedTokens: aggregate?.ratedTokens || 0,
|
|
1200
|
+
toolCalls: aggregate?.toolCalls || 0,
|
|
1201
|
+
eventCount,
|
|
1202
|
+
coverage: threadCoverage,
|
|
878
1203
|
});
|
|
879
1204
|
}
|
|
880
1205
|
threads.sort((left, right) => right.totalTokens - left.totalTokens);
|
|
1206
|
+
threadAggregates.clear();
|
|
1207
|
+
allThreadIds.clear();
|
|
1208
|
+
context.stateRows.clear();
|
|
1209
|
+
context.parents.clear();
|
|
881
1210
|
|
|
882
1211
|
const quotas = [...context.quotas.values()]
|
|
883
1212
|
.map((quota) => {
|
|
@@ -888,21 +1217,12 @@ function buildSnapshot(context, options, titles) {
|
|
|
888
1217
|
})
|
|
889
1218
|
.filter((quota) => {
|
|
890
1219
|
if (!options.since) return true;
|
|
891
|
-
return
|
|
1220
|
+
return Date.parse(quota.lastSeenAt) >= sinceMs;
|
|
892
1221
|
})
|
|
893
1222
|
.sort(
|
|
894
|
-
(left, right) =>
|
|
895
|
-
new Date(left.timestamp).getTime() -
|
|
896
|
-
new Date(right.timestamp).getTime(),
|
|
1223
|
+
(left, right) => Date.parse(left.timestamp) - Date.parse(right.timestamp),
|
|
897
1224
|
);
|
|
898
1225
|
|
|
899
|
-
const observedTokens = events.reduce(
|
|
900
|
-
(sum, event) => sum + event.totalTokens,
|
|
901
|
-
0,
|
|
902
|
-
);
|
|
903
|
-
const detailedTokens = events
|
|
904
|
-
.filter((event) => event.breakdownAvailable)
|
|
905
|
-
.reduce((sum, event) => sum + event.totalTokens, 0);
|
|
906
1226
|
const unknownBreakdownTokens = observedTokens - detailedTokens;
|
|
907
1227
|
const stateCounterSumNonAdditive = threads.reduce(
|
|
908
1228
|
(sum, thread) => sum + (thread.reportedCumulativeTokens || 0),
|
|
@@ -913,8 +1233,6 @@ function buildSnapshot(context, options, titles) {
|
|
|
913
1233
|
thread.coverage === "unresolved" &&
|
|
914
1234
|
(thread.reportedCumulativeTokens || 0) > 0,
|
|
915
1235
|
).length;
|
|
916
|
-
const earliestEventAt = events[0]?.timestamp || null;
|
|
917
|
-
const latestEventAt = events.at(-1)?.timestamp || null;
|
|
918
1236
|
const weeklyCandidates = quotas.filter(
|
|
919
1237
|
(quota) => quota.windowMinutes === WEEK_MINUTES,
|
|
920
1238
|
);
|
|
@@ -925,9 +1243,7 @@ function buildSnapshot(context, options, titles) {
|
|
|
925
1243
|
...(accountWideWeekly.length ? accountWideWeekly : weeklyCandidates),
|
|
926
1244
|
]
|
|
927
1245
|
.sort(
|
|
928
|
-
(left, right) =>
|
|
929
|
-
new Date(right.timestamp).getTime() -
|
|
930
|
-
new Date(left.timestamp).getTime(),
|
|
1246
|
+
(left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt),
|
|
931
1247
|
)[0];
|
|
932
1248
|
const weeklyStart = weekly
|
|
933
1249
|
? (weekly.resetsAt - weekly.windowMinutes * 60) * 1_000
|
|
@@ -935,19 +1251,20 @@ function buildSnapshot(context, options, titles) {
|
|
|
935
1251
|
const completeSinceWindowStart = Boolean(
|
|
936
1252
|
weeklyStart &&
|
|
937
1253
|
earliestEventAt &&
|
|
938
|
-
|
|
1254
|
+
Date.parse(earliestEventAt) <= weeklyStart &&
|
|
939
1255
|
context.parseErrors === 0,
|
|
940
1256
|
);
|
|
941
1257
|
|
|
942
1258
|
const notes = [
|
|
943
1259
|
"Observed totals sum globally de-duplicated last_token_usage model-call events.",
|
|
1260
|
+
"The snapshot keeps exact recent calls and compacts older usage into time buckets; token, cache, model, project, tool-call, and thread totals remain additive.",
|
|
944
1261
|
"Codex thread counters are retained only as non-additive reference values because forks and subagents inherit cumulative history.",
|
|
945
1262
|
"Historical rollout files can be pruned; a state-only counter cannot reveal that thread's unique token contribution.",
|
|
946
1263
|
"Legacy events without turn IDs use a high-specificity usage-signature heuristic and are labeled in the ledger.",
|
|
947
1264
|
];
|
|
948
1265
|
|
|
949
1266
|
return {
|
|
950
|
-
schemaVersion:
|
|
1267
|
+
schemaVersion: SNAPSHOT_SCHEMA_VERSION,
|
|
951
1268
|
generatedAt: new Date().toISOString(),
|
|
952
1269
|
label: "Local Codex snapshot",
|
|
953
1270
|
provenance: {
|
|
@@ -968,9 +1285,10 @@ function buildSnapshot(context, options, titles) {
|
|
|
968
1285
|
unknownBreakdownTokens,
|
|
969
1286
|
stateCounterSumNonAdditive,
|
|
970
1287
|
unresolvedThreadCounters,
|
|
971
|
-
legacyHeuristicEvents
|
|
972
|
-
|
|
973
|
-
|
|
1288
|
+
legacyHeuristicEvents,
|
|
1289
|
+
observedModelCalls,
|
|
1290
|
+
usageBucketCount: usageStats.bucketCount,
|
|
1291
|
+
maximumUsageResolutionSeconds: usageStats.maximumResolutionSeconds,
|
|
974
1292
|
detailedPercent:
|
|
975
1293
|
observedTokens > 0 ? (detailedTokens / observedTokens) * 100 : 100,
|
|
976
1294
|
earliestEventAt,
|
|
@@ -980,7 +1298,7 @@ function buildSnapshot(context, options, titles) {
|
|
|
980
1298
|
},
|
|
981
1299
|
quotaObservations: quotas,
|
|
982
1300
|
threads,
|
|
983
|
-
events,
|
|
1301
|
+
events: usageBuckets,
|
|
984
1302
|
};
|
|
985
1303
|
}
|
|
986
1304
|
|
|
@@ -999,14 +1317,13 @@ export async function collectUsage(options, onProgress = () => {}) {
|
|
|
999
1317
|
.flat()
|
|
1000
1318
|
.sort();
|
|
1001
1319
|
const sizes = await Promise.all(files.map((path) => stat(path)));
|
|
1320
|
+
const spool = await createUsageSpool();
|
|
1002
1321
|
|
|
1003
1322
|
const context = {
|
|
1004
1323
|
stateRows: state.rows,
|
|
1005
1324
|
parents: state.parents,
|
|
1006
|
-
origins: new Map(),
|
|
1007
|
-
tokens: new Map(),
|
|
1008
1325
|
quotas: new Map(),
|
|
1009
|
-
|
|
1326
|
+
spool,
|
|
1010
1327
|
filesScanned: 0,
|
|
1011
1328
|
bytesScanned: 0,
|
|
1012
1329
|
parseErrors: 0,
|
|
@@ -1014,24 +1331,28 @@ export async function collectUsage(options, onProgress = () => {}) {
|
|
|
1014
1331
|
correctionIntervals: 0,
|
|
1015
1332
|
};
|
|
1016
1333
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1334
|
+
try {
|
|
1335
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
1336
|
+
await scanRollout(files[index], context);
|
|
1337
|
+
context.filesScanned += 1;
|
|
1338
|
+
context.bytesScanned += sizes[index].size;
|
|
1339
|
+
if (
|
|
1340
|
+
index === 0 ||
|
|
1341
|
+
index === files.length - 1 ||
|
|
1342
|
+
(index + 1) % 10 === 0
|
|
1343
|
+
) {
|
|
1344
|
+
onProgress({
|
|
1345
|
+
current: index + 1,
|
|
1346
|
+
total: files.length,
|
|
1347
|
+
path: files[index],
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1031
1350
|
}
|
|
1351
|
+
return buildSnapshot(context, options, titles);
|
|
1352
|
+
} finally {
|
|
1353
|
+
spool.close();
|
|
1354
|
+
await rm(spool.directory, { recursive: true, force: true });
|
|
1032
1355
|
}
|
|
1033
|
-
|
|
1034
|
-
return buildSnapshot(context, options, titles);
|
|
1035
1356
|
}
|
|
1036
1357
|
|
|
1037
1358
|
async function main() {
|
|
@@ -1058,16 +1379,18 @@ async function main() {
|
|
|
1058
1379
|
);
|
|
1059
1380
|
});
|
|
1060
1381
|
process.stdout.write("\nToken Ledger: writing privacy-reduced snapshot…\n");
|
|
1061
|
-
await writePrivateSnapshot(options.output, snapshot);
|
|
1062
|
-
const
|
|
1382
|
+
const writeResult = await writePrivateSnapshot(options.output, snapshot);
|
|
1383
|
+
const storedSnapshot = writeResult.snapshot;
|
|
1063
1384
|
process.stdout.write(
|
|
1064
1385
|
[
|
|
1065
1386
|
`Snapshot: ${options.output}`,
|
|
1066
|
-
`Observed model-call tokens: ${
|
|
1067
|
-
`Unique
|
|
1068
|
-
`
|
|
1069
|
-
`
|
|
1070
|
-
`
|
|
1387
|
+
`Observed model-call tokens: ${storedSnapshot.coverage.observedTokens.toLocaleString()}`,
|
|
1388
|
+
`Unique model calls: ${storedSnapshot.coverage.observedModelCalls.toLocaleString()}`,
|
|
1389
|
+
`Stored usage buckets: ${storedSnapshot.events.length.toLocaleString()}`,
|
|
1390
|
+
`Duplicate/copied events skipped: ${storedSnapshot.coverage.duplicateEventsSkipped.toLocaleString()}`,
|
|
1391
|
+
`Threads with unresolved state-only counters: ${storedSnapshot.coverage.unresolvedThreadCounters.toLocaleString()}`,
|
|
1392
|
+
`Snapshot size: ${(writeResult.bytesWritten / 1_000_000).toFixed(1)} MB (${writeResult.encoding}; ${(writeResult.jsonBytes / 1_000_000).toFixed(1)} MB JSON before encoding)`,
|
|
1393
|
+
`Snapshot safety limit: ${(writeResult.maxBytes / 1_000_000).toFixed(1)} MB`,
|
|
1071
1394
|
].join("\n") + "\n",
|
|
1072
1395
|
);
|
|
1073
1396
|
}
|