tledger 0.2.1 → 0.3.0
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 +150 -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 +1527 -584
- package/bin/token-ledger-trend-terminal.mjs +73 -28
- package/bin/token-ledger-trend.mjs +25 -11
- package/bin/token-ledger-tui.mjs +7 -3
- package/bin/token-ledger.mjs +303 -92
- 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 +589 -279
- 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;
|
|
206
479
|
}
|
|
207
480
|
}
|
|
208
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
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return { label: text || null, subagent: null };
|
|
502
|
+
}
|
|
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,
|
|
@@ -460,11 +743,9 @@ function rememberQuota(quotaMap, rateLimits, occurrence) {
|
|
|
460
743
|
usedPercent,
|
|
461
744
|
windowMinutes,
|
|
462
745
|
resetsAt,
|
|
463
|
-
planType: String(
|
|
746
|
+
planType: String(planType || "unknown"),
|
|
464
747
|
limitKey: hash(limitKey, 16),
|
|
465
|
-
limitName:
|
|
466
|
-
? String(rateLimits.limit_name).slice(0, 80)
|
|
467
|
-
: null,
|
|
748
|
+
limitName: limitName ? String(limitName).slice(0, 80) : null,
|
|
468
749
|
source: "log",
|
|
469
750
|
turnId: occurrence.turnId || null,
|
|
470
751
|
originalLikely: occurrence.originalLikely,
|
|
@@ -514,6 +795,7 @@ async function scanRollout(path, context) {
|
|
|
514
795
|
const callOrdinals = new Map();
|
|
515
796
|
let currentTurnId = "";
|
|
516
797
|
let currentCandidate = null;
|
|
798
|
+
let currentCandidateSelected = false;
|
|
517
799
|
let previousCumulative = null;
|
|
518
800
|
|
|
519
801
|
const input = createReadStream(path, { encoding: "utf8" });
|
|
@@ -557,10 +839,7 @@ async function scanRollout(path, context) {
|
|
|
557
839
|
stateRow,
|
|
558
840
|
fileContext,
|
|
559
841
|
);
|
|
560
|
-
|
|
561
|
-
if (!currentBest || currentCandidate.deltaMs < currentBest.deltaMs) {
|
|
562
|
-
context.origins.set(currentTurnId, currentCandidate);
|
|
563
|
-
}
|
|
842
|
+
currentCandidateSelected = context.spool.insertOrigin(currentCandidate);
|
|
564
843
|
}
|
|
565
844
|
continue;
|
|
566
845
|
}
|
|
@@ -574,6 +853,9 @@ async function scanRollout(path, context) {
|
|
|
574
853
|
currentCandidate.model = fileContext.model;
|
|
575
854
|
currentCandidate.effort = fileContext.effort;
|
|
576
855
|
currentCandidate.cwd = fileContext.cwd;
|
|
856
|
+
if (currentCandidateSelected) {
|
|
857
|
+
context.spool.updateOrigin(currentCandidate);
|
|
858
|
+
}
|
|
577
859
|
}
|
|
578
860
|
continue;
|
|
579
861
|
}
|
|
@@ -617,14 +899,12 @@ async function scanRollout(path, context) {
|
|
|
617
899
|
const callKey = call.stableId
|
|
618
900
|
? `id|${call.stableId}`
|
|
619
901
|
: `ordinal|${ordinalBase}|${ordinal}`;
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
});
|
|
627
|
-
}
|
|
902
|
+
context.spool.insertCall(
|
|
903
|
+
callKey,
|
|
904
|
+
currentTurnId,
|
|
905
|
+
threadId,
|
|
906
|
+
originalLikely,
|
|
907
|
+
);
|
|
628
908
|
continue;
|
|
629
909
|
}
|
|
630
910
|
|
|
@@ -657,24 +937,16 @@ async function scanRollout(path, context) {
|
|
|
657
937
|
}
|
|
658
938
|
previousCumulative = totalTuple[5];
|
|
659
939
|
|
|
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),
|
|
940
|
+
const inserted = context.spool.insertToken(
|
|
941
|
+
eventKey,
|
|
942
|
+
currentTurnId,
|
|
943
|
+
usageFromTuple(lastTuple),
|
|
674
944
|
occurrence,
|
|
675
945
|
originalLikely,
|
|
676
|
-
|
|
677
|
-
|
|
946
|
+
);
|
|
947
|
+
if (!inserted) {
|
|
948
|
+
context.duplicateEventsSkipped += 1;
|
|
949
|
+
}
|
|
678
950
|
}
|
|
679
951
|
}
|
|
680
952
|
|
|
@@ -708,144 +980,183 @@ function threadMetadata(threadId, stateRows, titles, parents, fallback = {}) {
|
|
|
708
980
|
};
|
|
709
981
|
}
|
|
710
982
|
|
|
983
|
+
function resolvedOccurrence(token) {
|
|
984
|
+
const origin = token.originThreadId == null
|
|
985
|
+
? null
|
|
986
|
+
: {
|
|
987
|
+
threadId: token.originThreadId,
|
|
988
|
+
timestamp: token.originTimestamp,
|
|
989
|
+
model: token.originModel,
|
|
990
|
+
effort: token.originEffort,
|
|
991
|
+
cwd: token.originCwd,
|
|
992
|
+
gitOrigin: token.originGitOrigin,
|
|
993
|
+
rawSource: token.originRawSource,
|
|
994
|
+
serviceTier: token.originServiceTier,
|
|
995
|
+
};
|
|
996
|
+
const occurrence = token.originalLikely || !origin ? token : origin;
|
|
997
|
+
return {
|
|
998
|
+
origin,
|
|
999
|
+
occurrence,
|
|
1000
|
+
threadId: origin?.threadId || occurrence.threadId,
|
|
1001
|
+
timestamp: token.timestamp || origin?.timestamp,
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function newThreadAggregate(metadata, timestamp) {
|
|
1006
|
+
return {
|
|
1007
|
+
metadata,
|
|
1008
|
+
firstActiveAt: timestamp,
|
|
1009
|
+
lastActiveAt: timestamp,
|
|
1010
|
+
inputTokens: 0,
|
|
1011
|
+
cachedInputTokens: 0,
|
|
1012
|
+
outputTokens: 0,
|
|
1013
|
+
reasoningTokens: 0,
|
|
1014
|
+
totalTokens: 0,
|
|
1015
|
+
toolCalls: 0,
|
|
1016
|
+
detailedTokens: 0,
|
|
1017
|
+
unknownBreakdownTokens: 0,
|
|
1018
|
+
rateCardCredits: 0,
|
|
1019
|
+
ratedTokens: 0,
|
|
1020
|
+
eventCount: 0,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function addToThreadAggregate(aggregate, event) {
|
|
1025
|
+
aggregate.lastActiveAt = event.timestamp;
|
|
1026
|
+
aggregate.inputTokens += event.inputTokens;
|
|
1027
|
+
aggregate.cachedInputTokens += event.cachedInputTokens;
|
|
1028
|
+
aggregate.outputTokens += event.outputTokens;
|
|
1029
|
+
aggregate.reasoningTokens += event.reasoningTokens;
|
|
1030
|
+
aggregate.totalTokens += event.totalTokens;
|
|
1031
|
+
aggregate.toolCalls += event.toolCalls;
|
|
1032
|
+
aggregate.eventCount += 1;
|
|
1033
|
+
if (event.breakdownAvailable) {
|
|
1034
|
+
aggregate.detailedTokens += event.totalTokens;
|
|
1035
|
+
} else {
|
|
1036
|
+
aggregate.unknownBreakdownTokens += event.totalTokens;
|
|
1037
|
+
}
|
|
1038
|
+
if (event.rateCardCredits !== null) {
|
|
1039
|
+
aggregate.rateCardCredits += event.rateCardCredits;
|
|
1040
|
+
aggregate.ratedTokens += event.totalTokens;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
711
1044
|
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
|
-
});
|
|
1045
|
+
context.spool.finishWrites();
|
|
1046
|
+
const sinceMs = options.since ? options.since.getTime() : -Infinity;
|
|
1047
|
+
const lastEventKeyByTurn = new Map();
|
|
1048
|
+
let earliestEventAt = null;
|
|
1049
|
+
let latestEventAt = null;
|
|
1050
|
+
|
|
1051
|
+
// The spool is ordered on disk. This lightweight pass finds the event that
|
|
1052
|
+
// receives each turn's tool calls without retaining the usage rows.
|
|
1053
|
+
for (const token of context.spool.tokenRows()) {
|
|
1054
|
+
const { threadId, timestamp } = resolvedOccurrence(token);
|
|
1055
|
+
if (Date.parse(timestamp) < sinceMs) continue;
|
|
1056
|
+
earliestEventAt ||= timestamp;
|
|
1057
|
+
latestEventAt = timestamp;
|
|
1058
|
+
const key = token.turnId || `thread:${threadId}`;
|
|
1059
|
+
lastEventKeyByTurn.set(key, token.eventKey);
|
|
764
1060
|
}
|
|
765
|
-
events.sort(
|
|
766
|
-
(left, right) =>
|
|
767
|
-
new Date(left.timestamp).getTime() - new Date(right.timestamp).getTime(),
|
|
768
|
-
);
|
|
769
1061
|
|
|
770
1062
|
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}`;
|
|
1063
|
+
for (const call of context.spool.callRows()) {
|
|
1064
|
+
const key = call.turnId || `thread:${call.threadId}`;
|
|
775
1065
|
toolCounts.set(key, (toolCounts.get(key) || 0) + 1);
|
|
776
1066
|
}
|
|
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
1067
|
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1068
|
+
const threadAggregates = new Map();
|
|
1069
|
+
let observedTokens = 0;
|
|
1070
|
+
let detailedTokens = 0;
|
|
1071
|
+
let legacyHeuristicEvents = 0;
|
|
1072
|
+
let observedModelCalls = 0;
|
|
1073
|
+
|
|
1074
|
+
function* compactableEvents() {
|
|
1075
|
+
for (const token of context.spool.tokenRows()) {
|
|
1076
|
+
const { origin, occurrence, threadId, timestamp } = resolvedOccurrence(token);
|
|
1077
|
+
if (Date.parse(timestamp) < sinceMs) continue;
|
|
1078
|
+
const metadata = threadMetadata(
|
|
1079
|
+
threadId,
|
|
1080
|
+
context.stateRows,
|
|
1081
|
+
titles,
|
|
1082
|
+
context.parents,
|
|
1083
|
+
origin || occurrence,
|
|
1084
|
+
);
|
|
1085
|
+
const usage = {
|
|
1086
|
+
inputTokens: Number(token.inputTokens),
|
|
1087
|
+
cachedInputTokens: Number(token.cachedInputTokens),
|
|
1088
|
+
cacheWriteInputTokens: Number(token.cacheWriteInputTokens),
|
|
1089
|
+
outputTokens: Number(token.outputTokens),
|
|
1090
|
+
reasoningTokens: Number(token.reasoningTokens),
|
|
1091
|
+
totalTokens: Number(token.totalTokens),
|
|
1092
|
+
};
|
|
1093
|
+
const breakdownAvailable = hasDetailedBreakdown(usage);
|
|
1094
|
+
const serviceTier = occurrence.serviceTier || null;
|
|
1095
|
+
const baseCredits = creditsForUsage(metadata.model, usage);
|
|
1096
|
+
const turnKey = token.turnId || `thread:${threadId}`;
|
|
1097
|
+
const event = {
|
|
1098
|
+
...usage,
|
|
1099
|
+
timestamp,
|
|
1100
|
+
threadId,
|
|
1101
|
+
project: metadata.project,
|
|
1102
|
+
model: metadata.model,
|
|
1103
|
+
effort: metadata.effort,
|
|
1104
|
+
source: metadata.source,
|
|
1105
|
+
useType: metadata.useType,
|
|
1106
|
+
toolCalls:
|
|
1107
|
+
lastEventKeyByTurn.get(turnKey) === token.eventKey
|
|
1108
|
+
? toolCounts.get(turnKey) || 0
|
|
1109
|
+
: 0,
|
|
1110
|
+
serviceTier,
|
|
1111
|
+
rateCardCredits:
|
|
1112
|
+
baseCredits === null
|
|
1113
|
+
? null
|
|
1114
|
+
: serviceTier === "priority"
|
|
1115
|
+
? baseCredits * FAST_MODE_MULTIPLIER
|
|
1116
|
+
: baseCredits,
|
|
1117
|
+
breakdownAvailable,
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
let aggregate = threadAggregates.get(threadId);
|
|
1121
|
+
if (!aggregate) {
|
|
1122
|
+
aggregate = newThreadAggregate(metadata, timestamp);
|
|
1123
|
+
threadAggregates.set(threadId, aggregate);
|
|
1124
|
+
}
|
|
1125
|
+
addToThreadAggregate(aggregate, event);
|
|
1126
|
+
observedTokens += event.totalTokens;
|
|
1127
|
+
if (breakdownAvailable) detailedTokens += event.totalTokens;
|
|
1128
|
+
if (!token.turnId) legacyHeuristicEvents += 1;
|
|
1129
|
+
observedModelCalls += 1;
|
|
1130
|
+
yield event;
|
|
1131
|
+
}
|
|
792
1132
|
}
|
|
793
1133
|
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1134
|
+
const usageBuckets = buildUsageBuckets(compactableEvents(), {
|
|
1135
|
+
latestTimestampMs: latestEventAt ? Date.parse(latestEventAt) : 0,
|
|
1136
|
+
});
|
|
1137
|
+
const usageStats = usageBucketStats(usageBuckets);
|
|
1138
|
+
toolCounts.clear();
|
|
1139
|
+
lastEventKeyByTurn.clear();
|
|
1140
|
+
|
|
1141
|
+
const allThreadIds = new Set(context.stateRows.keys());
|
|
1142
|
+
for (const threadId of threadAggregates.keys()) allThreadIds.add(threadId);
|
|
798
1143
|
const threads = [];
|
|
799
1144
|
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(
|
|
1145
|
+
const aggregate = threadAggregates.get(threadId);
|
|
1146
|
+
const metadata = aggregate?.metadata || threadMetadata(
|
|
807
1147
|
threadId,
|
|
808
1148
|
context.stateRows,
|
|
809
1149
|
titles,
|
|
810
1150
|
context.parents,
|
|
811
|
-
origin || first || {},
|
|
812
1151
|
);
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
);
|
|
842
|
-
if (rows.length === 0 && !(metadata.reportedCumulativeTokens > 0)) continue;
|
|
843
|
-
const coverage =
|
|
844
|
-
rows.length === 0
|
|
1152
|
+
if (!aggregate && !(metadata.reportedCumulativeTokens > 0)) continue;
|
|
1153
|
+
const eventCount = aggregate?.eventCount || 0;
|
|
1154
|
+
const threadCoverage =
|
|
1155
|
+
eventCount === 0
|
|
845
1156
|
? "unresolved"
|
|
846
|
-
:
|
|
1157
|
+
: aggregate.detailedTokens === aggregate.totalTokens
|
|
847
1158
|
? "complete"
|
|
848
|
-
:
|
|
1159
|
+
: aggregate.detailedTokens > 0
|
|
849
1160
|
? "partial"
|
|
850
1161
|
: "total-only";
|
|
851
1162
|
threads.push({
|
|
@@ -857,27 +1168,32 @@ function buildSnapshot(context, options, titles) {
|
|
|
857
1168
|
source: metadata.source,
|
|
858
1169
|
useType: metadata.useType,
|
|
859
1170
|
parentThreadId: metadata.parentThreadId,
|
|
860
|
-
firstActiveAt:
|
|
861
|
-
lastActiveAt:
|
|
862
|
-
totalTokens:
|
|
863
|
-
detailedTokens:
|
|
864
|
-
unknownBreakdownTokens:
|
|
1171
|
+
firstActiveAt: aggregate?.firstActiveAt || metadata.createdAt,
|
|
1172
|
+
lastActiveAt: aggregate?.lastActiveAt || metadata.updatedAt,
|
|
1173
|
+
totalTokens: aggregate?.totalTokens || 0,
|
|
1174
|
+
detailedTokens: aggregate?.detailedTokens || 0,
|
|
1175
|
+
unknownBreakdownTokens: aggregate?.unknownBreakdownTokens || 0,
|
|
865
1176
|
reportedCumulativeTokens: metadata.reportedCumulativeTokens,
|
|
866
|
-
inputTokens:
|
|
867
|
-
cachedInputTokens:
|
|
868
|
-
outputTokens:
|
|
869
|
-
reasoningTokens:
|
|
1177
|
+
inputTokens: aggregate?.inputTokens || 0,
|
|
1178
|
+
cachedInputTokens: aggregate?.cachedInputTokens || 0,
|
|
1179
|
+
outputTokens: aggregate?.outputTokens || 0,
|
|
1180
|
+
reasoningTokens: aggregate?.reasoningTokens || 0,
|
|
870
1181
|
rateCardCredits:
|
|
871
|
-
|
|
872
|
-
|
|
1182
|
+
aggregate?.totalTokens > 0 &&
|
|
1183
|
+
aggregate.ratedTokens === aggregate.totalTokens
|
|
1184
|
+
? aggregate.rateCardCredits
|
|
873
1185
|
: null,
|
|
874
|
-
ratedTokens:
|
|
875
|
-
toolCalls:
|
|
876
|
-
eventCount
|
|
877
|
-
coverage,
|
|
1186
|
+
ratedTokens: aggregate?.ratedTokens || 0,
|
|
1187
|
+
toolCalls: aggregate?.toolCalls || 0,
|
|
1188
|
+
eventCount,
|
|
1189
|
+
coverage: threadCoverage,
|
|
878
1190
|
});
|
|
879
1191
|
}
|
|
880
1192
|
threads.sort((left, right) => right.totalTokens - left.totalTokens);
|
|
1193
|
+
threadAggregates.clear();
|
|
1194
|
+
allThreadIds.clear();
|
|
1195
|
+
context.stateRows.clear();
|
|
1196
|
+
context.parents.clear();
|
|
881
1197
|
|
|
882
1198
|
const quotas = [...context.quotas.values()]
|
|
883
1199
|
.map((quota) => {
|
|
@@ -888,21 +1204,12 @@ function buildSnapshot(context, options, titles) {
|
|
|
888
1204
|
})
|
|
889
1205
|
.filter((quota) => {
|
|
890
1206
|
if (!options.since) return true;
|
|
891
|
-
return
|
|
1207
|
+
return Date.parse(quota.timestamp) >= sinceMs;
|
|
892
1208
|
})
|
|
893
1209
|
.sort(
|
|
894
|
-
(left, right) =>
|
|
895
|
-
new Date(left.timestamp).getTime() -
|
|
896
|
-
new Date(right.timestamp).getTime(),
|
|
1210
|
+
(left, right) => Date.parse(left.timestamp) - Date.parse(right.timestamp),
|
|
897
1211
|
);
|
|
898
1212
|
|
|
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
1213
|
const unknownBreakdownTokens = observedTokens - detailedTokens;
|
|
907
1214
|
const stateCounterSumNonAdditive = threads.reduce(
|
|
908
1215
|
(sum, thread) => sum + (thread.reportedCumulativeTokens || 0),
|
|
@@ -913,8 +1220,6 @@ function buildSnapshot(context, options, titles) {
|
|
|
913
1220
|
thread.coverage === "unresolved" &&
|
|
914
1221
|
(thread.reportedCumulativeTokens || 0) > 0,
|
|
915
1222
|
).length;
|
|
916
|
-
const earliestEventAt = events[0]?.timestamp || null;
|
|
917
|
-
const latestEventAt = events.at(-1)?.timestamp || null;
|
|
918
1223
|
const weeklyCandidates = quotas.filter(
|
|
919
1224
|
(quota) => quota.windowMinutes === WEEK_MINUTES,
|
|
920
1225
|
);
|
|
@@ -925,9 +1230,7 @@ function buildSnapshot(context, options, titles) {
|
|
|
925
1230
|
...(accountWideWeekly.length ? accountWideWeekly : weeklyCandidates),
|
|
926
1231
|
]
|
|
927
1232
|
.sort(
|
|
928
|
-
(left, right) =>
|
|
929
|
-
new Date(right.timestamp).getTime() -
|
|
930
|
-
new Date(left.timestamp).getTime(),
|
|
1233
|
+
(left, right) => Date.parse(right.timestamp) - Date.parse(left.timestamp),
|
|
931
1234
|
)[0];
|
|
932
1235
|
const weeklyStart = weekly
|
|
933
1236
|
? (weekly.resetsAt - weekly.windowMinutes * 60) * 1_000
|
|
@@ -935,19 +1238,20 @@ function buildSnapshot(context, options, titles) {
|
|
|
935
1238
|
const completeSinceWindowStart = Boolean(
|
|
936
1239
|
weeklyStart &&
|
|
937
1240
|
earliestEventAt &&
|
|
938
|
-
|
|
1241
|
+
Date.parse(earliestEventAt) <= weeklyStart &&
|
|
939
1242
|
context.parseErrors === 0,
|
|
940
1243
|
);
|
|
941
1244
|
|
|
942
1245
|
const notes = [
|
|
943
1246
|
"Observed totals sum globally de-duplicated last_token_usage model-call events.",
|
|
1247
|
+
"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
1248
|
"Codex thread counters are retained only as non-additive reference values because forks and subagents inherit cumulative history.",
|
|
945
1249
|
"Historical rollout files can be pruned; a state-only counter cannot reveal that thread's unique token contribution.",
|
|
946
1250
|
"Legacy events without turn IDs use a high-specificity usage-signature heuristic and are labeled in the ledger.",
|
|
947
1251
|
];
|
|
948
1252
|
|
|
949
1253
|
return {
|
|
950
|
-
schemaVersion:
|
|
1254
|
+
schemaVersion: SNAPSHOT_SCHEMA_VERSION,
|
|
951
1255
|
generatedAt: new Date().toISOString(),
|
|
952
1256
|
label: "Local Codex snapshot",
|
|
953
1257
|
provenance: {
|
|
@@ -968,9 +1272,10 @@ function buildSnapshot(context, options, titles) {
|
|
|
968
1272
|
unknownBreakdownTokens,
|
|
969
1273
|
stateCounterSumNonAdditive,
|
|
970
1274
|
unresolvedThreadCounters,
|
|
971
|
-
legacyHeuristicEvents
|
|
972
|
-
|
|
973
|
-
|
|
1275
|
+
legacyHeuristicEvents,
|
|
1276
|
+
observedModelCalls,
|
|
1277
|
+
usageBucketCount: usageStats.bucketCount,
|
|
1278
|
+
maximumUsageResolutionSeconds: usageStats.maximumResolutionSeconds,
|
|
974
1279
|
detailedPercent:
|
|
975
1280
|
observedTokens > 0 ? (detailedTokens / observedTokens) * 100 : 100,
|
|
976
1281
|
earliestEventAt,
|
|
@@ -980,7 +1285,7 @@ function buildSnapshot(context, options, titles) {
|
|
|
980
1285
|
},
|
|
981
1286
|
quotaObservations: quotas,
|
|
982
1287
|
threads,
|
|
983
|
-
events,
|
|
1288
|
+
events: usageBuckets,
|
|
984
1289
|
};
|
|
985
1290
|
}
|
|
986
1291
|
|
|
@@ -999,14 +1304,13 @@ export async function collectUsage(options, onProgress = () => {}) {
|
|
|
999
1304
|
.flat()
|
|
1000
1305
|
.sort();
|
|
1001
1306
|
const sizes = await Promise.all(files.map((path) => stat(path)));
|
|
1307
|
+
const spool = await createUsageSpool();
|
|
1002
1308
|
|
|
1003
1309
|
const context = {
|
|
1004
1310
|
stateRows: state.rows,
|
|
1005
1311
|
parents: state.parents,
|
|
1006
|
-
origins: new Map(),
|
|
1007
|
-
tokens: new Map(),
|
|
1008
1312
|
quotas: new Map(),
|
|
1009
|
-
|
|
1313
|
+
spool,
|
|
1010
1314
|
filesScanned: 0,
|
|
1011
1315
|
bytesScanned: 0,
|
|
1012
1316
|
parseErrors: 0,
|
|
@@ -1014,24 +1318,28 @@ export async function collectUsage(options, onProgress = () => {}) {
|
|
|
1014
1318
|
correctionIntervals: 0,
|
|
1015
1319
|
};
|
|
1016
1320
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1321
|
+
try {
|
|
1322
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
1323
|
+
await scanRollout(files[index], context);
|
|
1324
|
+
context.filesScanned += 1;
|
|
1325
|
+
context.bytesScanned += sizes[index].size;
|
|
1326
|
+
if (
|
|
1327
|
+
index === 0 ||
|
|
1328
|
+
index === files.length - 1 ||
|
|
1329
|
+
(index + 1) % 10 === 0
|
|
1330
|
+
) {
|
|
1331
|
+
onProgress({
|
|
1332
|
+
current: index + 1,
|
|
1333
|
+
total: files.length,
|
|
1334
|
+
path: files[index],
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1031
1337
|
}
|
|
1338
|
+
return buildSnapshot(context, options, titles);
|
|
1339
|
+
} finally {
|
|
1340
|
+
spool.close();
|
|
1341
|
+
await rm(spool.directory, { recursive: true, force: true });
|
|
1032
1342
|
}
|
|
1033
|
-
|
|
1034
|
-
return buildSnapshot(context, options, titles);
|
|
1035
1343
|
}
|
|
1036
1344
|
|
|
1037
1345
|
async function main() {
|
|
@@ -1058,16 +1366,18 @@ async function main() {
|
|
|
1058
1366
|
);
|
|
1059
1367
|
});
|
|
1060
1368
|
process.stdout.write("\nToken Ledger: writing privacy-reduced snapshot…\n");
|
|
1061
|
-
await writePrivateSnapshot(options.output, snapshot);
|
|
1062
|
-
const
|
|
1369
|
+
const writeResult = await writePrivateSnapshot(options.output, snapshot);
|
|
1370
|
+
const storedSnapshot = writeResult.snapshot;
|
|
1063
1371
|
process.stdout.write(
|
|
1064
1372
|
[
|
|
1065
1373
|
`Snapshot: ${options.output}`,
|
|
1066
|
-
`Observed model-call tokens: ${
|
|
1067
|
-
`Unique
|
|
1068
|
-
`
|
|
1069
|
-
`
|
|
1070
|
-
`
|
|
1374
|
+
`Observed model-call tokens: ${storedSnapshot.coverage.observedTokens.toLocaleString()}`,
|
|
1375
|
+
`Unique model calls: ${storedSnapshot.coverage.observedModelCalls.toLocaleString()}`,
|
|
1376
|
+
`Stored usage buckets: ${storedSnapshot.events.length.toLocaleString()}`,
|
|
1377
|
+
`Duplicate/copied events skipped: ${storedSnapshot.coverage.duplicateEventsSkipped.toLocaleString()}`,
|
|
1378
|
+
`Threads with unresolved state-only counters: ${storedSnapshot.coverage.unresolvedThreadCounters.toLocaleString()}`,
|
|
1379
|
+
`Snapshot size: ${(writeResult.bytesWritten / 1_000_000).toFixed(1)} MB (${writeResult.encoding}; ${(writeResult.jsonBytes / 1_000_000).toFixed(1)} MB JSON before encoding)`,
|
|
1380
|
+
`Snapshot safety limit: ${(writeResult.maxBytes / 1_000_000).toFixed(1)} MB`,
|
|
1071
1381
|
].join("\n") + "\n",
|
|
1072
1382
|
);
|
|
1073
1383
|
}
|