tledger 0.3.1 → 0.4.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 +283 -157
- package/bin/token-ledger-cache-data.mjs +492 -0
- package/bin/token-ledger-cache-image.mjs +7 -1147
- package/bin/token-ledger-cache-sections.mjs +848 -0
- package/bin/token-ledger-cost-terminal.mjs +234 -0
- package/bin/token-ledger-image-layout.mjs +20 -0
- package/bin/token-ledger-image-primitives.mjs +192 -0
- package/bin/token-ledger-report-data.mjs +1159 -0
- package/bin/token-ledger-source-status.mjs +31 -0
- package/bin/token-ledger-terminal.mjs +245 -69
- package/bin/token-ledger-trend-image.mjs +2025 -1724
- package/bin/token-ledger-trend-terminal.mjs +273 -150
- package/bin/token-ledger-trend.mjs +204 -198
- package/bin/token-ledger-tui.mjs +180 -51
- package/bin/token-ledger.mjs +735 -208
- package/docs/durable-ledger-operations.md +198 -0
- package/docs/release-notes-0.4.0.md +41 -0
- package/docs/token-ledger-report-7-day.png +0 -0
- package/lib/token-ledger-calendar.mjs +225 -0
- package/lib/token-ledger-collection.mjs +100 -0
- package/lib/token-ledger-importer.mjs +3329 -488
- package/lib/token-ledger-labels.mjs +66 -0
- package/lib/token-ledger-ledger.mjs +6056 -0
- package/lib/token-ledger-quota-contract.mjs +38 -0
- package/lib/token-ledger-range-analysis.mjs +120 -0
- package/lib/token-ledger-rates.mjs +330 -0
- package/lib/token-ledger-snapshot.mjs +336 -35
- package/lib/token-ledger-terminal-text.mjs +11 -0
- package/lib/token-ledger-usage.mjs +339 -33
- package/package.json +13 -10
- package/bin/token-ledger-rates.mjs +0 -65
|
@@ -15,7 +15,10 @@ import { createHash } from "node:crypto";
|
|
|
15
15
|
import { createReadStream } from "node:fs";
|
|
16
16
|
import {
|
|
17
17
|
access,
|
|
18
|
+
chmod,
|
|
19
|
+
lstat,
|
|
18
20
|
mkdtemp,
|
|
21
|
+
open,
|
|
19
22
|
readdir,
|
|
20
23
|
rm,
|
|
21
24
|
stat,
|
|
@@ -24,25 +27,94 @@ import { homedir, tmpdir } from "node:os";
|
|
|
24
27
|
import {
|
|
25
28
|
basename,
|
|
26
29
|
extname,
|
|
30
|
+
relative,
|
|
27
31
|
resolve,
|
|
28
32
|
} from "node:path";
|
|
29
33
|
import { pathToFileURL } from "node:url";
|
|
30
34
|
import { createInterface } from "node:readline";
|
|
31
35
|
import { DatabaseSync } from "node:sqlite";
|
|
36
|
+
import { Readable, Transform } from "node:stream";
|
|
37
|
+
import {
|
|
38
|
+
isMainThread,
|
|
39
|
+
parentPort,
|
|
40
|
+
Worker,
|
|
41
|
+
workerData,
|
|
42
|
+
} from "node:worker_threads";
|
|
32
43
|
|
|
33
|
-
import {
|
|
44
|
+
import { stagePrivateSnapshot } from "./token-ledger-snapshot.mjs";
|
|
45
|
+
import {
|
|
46
|
+
containsLocalPath,
|
|
47
|
+
LOCAL_LABEL,
|
|
48
|
+
safeExportLabel,
|
|
49
|
+
} from "./token-ledger-labels.mjs";
|
|
50
|
+
import {
|
|
51
|
+
DURABLE_LEDGER_COMPACTED_RETENTION_DAYS,
|
|
52
|
+
DURABLE_LEDGER_RETENTION_DAYS,
|
|
53
|
+
DURABLE_LEDGER_SCHEMA_VERSION,
|
|
54
|
+
codexHomeFingerprint,
|
|
55
|
+
durableSourceId,
|
|
56
|
+
durableQuotaObservationKey,
|
|
57
|
+
normalizeQuotaObservationFields,
|
|
58
|
+
readDurableSourceContinuity,
|
|
59
|
+
resolveDurableLedgerPath,
|
|
60
|
+
updateDurableLedger,
|
|
61
|
+
} from "./token-ledger-ledger.mjs";
|
|
62
|
+
import {
|
|
63
|
+
QUOTA_IDENTITY_CONTRACT_VERSION,
|
|
64
|
+
} from "./token-ledger-quota-contract.mjs";
|
|
65
|
+
import {
|
|
66
|
+
calculateCodexPurchasedCredits,
|
|
67
|
+
CODEX_CREDIT_RATE_CARD_AS_OF,
|
|
68
|
+
CODEX_CREDIT_RATE_CARD_KIND,
|
|
69
|
+
CODEX_CREDIT_RATE_CARD_SCOPE,
|
|
70
|
+
CODEX_CREDIT_RATE_CARD_URL,
|
|
71
|
+
hasDetailedTokenBreakdown,
|
|
72
|
+
normalizeCodexCreditModel,
|
|
73
|
+
} from "./token-ledger-rates.mjs";
|
|
74
|
+
import {
|
|
75
|
+
collectionScope,
|
|
76
|
+
normalizeCollectionSince,
|
|
77
|
+
} from "./token-ledger-collection.mjs";
|
|
34
78
|
import {
|
|
35
79
|
buildUsageBuckets,
|
|
80
|
+
checkedFiniteAdd,
|
|
81
|
+
checkedTokenPartitionAdd,
|
|
82
|
+
checkedTokenAdd,
|
|
83
|
+
isValidTokenValue,
|
|
84
|
+
MAX_SAFE_TOKEN_COUNT,
|
|
36
85
|
SNAPSHOT_SCHEMA_VERSION,
|
|
86
|
+
splitUsageBucketsAtBoundaries,
|
|
87
|
+
tokenValue,
|
|
37
88
|
usageBucketStats,
|
|
38
89
|
} from "./token-ledger-usage.mjs";
|
|
39
|
-
|
|
40
|
-
const RATE_CARD_AS_OF = "2026-08-17";
|
|
41
|
-
const FAST_MODE_MULTIPLIER = 1.5;
|
|
42
|
-
const RATE_CARD_URL = "https://help.openai.com/en/articles/20001106";
|
|
43
90
|
const WEEK_MINUTES = 10_080;
|
|
91
|
+
const SCAN_CONCURRENCY = 4;
|
|
92
|
+
const USAGE_SPOOL_DIRECTORY = /^token-ledger-import-([1-9]\d*)-[A-Za-z0-9]{6}$/;
|
|
44
93
|
const UUID_AT_END =
|
|
45
94
|
/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
|
|
95
|
+
const RELEVANT_EVENT_TYPES = new Set([
|
|
96
|
+
"task_started",
|
|
97
|
+
"thread_settings_applied",
|
|
98
|
+
"token_count",
|
|
99
|
+
]);
|
|
100
|
+
const RELEVANT_CALL_TYPES = new Set([
|
|
101
|
+
"function_call",
|
|
102
|
+
"custom_tool_call",
|
|
103
|
+
"tool_search_call",
|
|
104
|
+
"web_search_call",
|
|
105
|
+
"image_generation_call",
|
|
106
|
+
]);
|
|
107
|
+
export const SOURCE_WATERMARK_VERSION = 1;
|
|
108
|
+
export const SOURCE_COLLECTION_MAX_ATTEMPTS = 3;
|
|
109
|
+
const SOURCE_MUTATION_ERROR_CODES = Object.freeze([
|
|
110
|
+
"ENOENT",
|
|
111
|
+
"ENOTDIR",
|
|
112
|
+
"EISDIR",
|
|
113
|
+
"ERR_SOURCE_IDENTITY_CHANGED",
|
|
114
|
+
"ERR_SOURCE_SIZE_CHANGED",
|
|
115
|
+
"ERR_SOURCE_CONTENT_CHANGED",
|
|
116
|
+
]);
|
|
117
|
+
const MAX_TRAILING_SCAN_BYTES = 1 * 1024 * 1024;
|
|
46
118
|
|
|
47
119
|
function spoolText(value, maximumLength = 2_000) {
|
|
48
120
|
try {
|
|
@@ -55,13 +127,119 @@ function spoolText(value, maximumLength = 2_000) {
|
|
|
55
127
|
function spoolSource(value) {
|
|
56
128
|
const descriptor = sourceDescriptor(value);
|
|
57
129
|
if (descriptor.subagent) return '{"subagent":true}';
|
|
58
|
-
|
|
130
|
+
if (containsLocalPath(descriptor.label)) return "/";
|
|
131
|
+
return safeExportLabel(descriptor.label, 80, "");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function spoolCwd(value) {
|
|
135
|
+
const path = spoolText(value).replaceAll("\\", "/").replace(/\/+$/, "");
|
|
136
|
+
return safeExportLabel(basename(path), 160, "");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function spoolGitOrigin(value) {
|
|
140
|
+
const origin = spoolText(value).trim();
|
|
141
|
+
return origin ? projectLabel("", origin) : "";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function processIsAlive(pid) {
|
|
145
|
+
try {
|
|
146
|
+
process.kill(pid, 0);
|
|
147
|
+
return true;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
// EPERM still proves that the PID exists. Only ESRCH is authoritative
|
|
150
|
+
// evidence that no process currently owns the spool namespace.
|
|
151
|
+
return error?.code !== "ESRCH";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function cleanupOrphanedUsageSpools({
|
|
156
|
+
tempDirectory = tmpdir(),
|
|
157
|
+
currentPid = process.pid,
|
|
158
|
+
currentUid = process.getuid?.(),
|
|
159
|
+
beforeRevalidate = null,
|
|
160
|
+
} = {}) {
|
|
161
|
+
if (!Number.isSafeInteger(currentUid) || currentUid < 0) return 0;
|
|
162
|
+
let entries;
|
|
163
|
+
try {
|
|
164
|
+
entries = await readdir(tempDirectory, { withFileTypes: true });
|
|
165
|
+
} catch {
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
let removed = 0;
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
const match = String(entry.name).match(USAGE_SPOOL_DIRECTORY);
|
|
171
|
+
if (!match) continue;
|
|
172
|
+
const ownerPid = Number(match[1]);
|
|
173
|
+
if (
|
|
174
|
+
!Number.isSafeInteger(ownerPid) ||
|
|
175
|
+
ownerPid <= 0 ||
|
|
176
|
+
ownerPid === currentPid ||
|
|
177
|
+
processIsAlive(ownerPid)
|
|
178
|
+
) continue;
|
|
179
|
+
const candidate = resolve(tempDirectory, entry.name);
|
|
180
|
+
let initial;
|
|
181
|
+
try {
|
|
182
|
+
initial = await lstat(candidate);
|
|
183
|
+
} catch {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (
|
|
187
|
+
!initial.isDirectory() ||
|
|
188
|
+
initial.isSymbolicLink() ||
|
|
189
|
+
Number(initial.uid) !== currentUid
|
|
190
|
+
) continue;
|
|
191
|
+
|
|
192
|
+
if (beforeRevalidate !== null) await beforeRevalidate(candidate);
|
|
193
|
+
// Revalidate the exact directory immediately before removal. Node's
|
|
194
|
+
// recursive rm unlinks symlinks rather than traversing them, and the
|
|
195
|
+
// inode check also refuses a root-path swap between inspection steps.
|
|
196
|
+
let current;
|
|
197
|
+
try {
|
|
198
|
+
current = await lstat(candidate);
|
|
199
|
+
} catch {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (
|
|
203
|
+
!current.isDirectory() ||
|
|
204
|
+
current.isSymbolicLink() ||
|
|
205
|
+
Number(current.uid) !== currentUid ||
|
|
206
|
+
Number(current.dev) !== Number(initial.dev) ||
|
|
207
|
+
Number(current.ino) !== Number(initial.ino) ||
|
|
208
|
+
processIsAlive(ownerPid)
|
|
209
|
+
) continue;
|
|
210
|
+
try {
|
|
211
|
+
await rm(candidate, { recursive: true, force: false });
|
|
212
|
+
removed += 1;
|
|
213
|
+
} catch {
|
|
214
|
+
// Cleanup is best-effort. The new private spool remains usable even if
|
|
215
|
+
// an orphan races away or local permissions prevent reclamation.
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return removed;
|
|
59
219
|
}
|
|
60
220
|
|
|
61
221
|
async function createUsageSpool() {
|
|
62
|
-
|
|
222
|
+
await cleanupOrphanedUsageSpools();
|
|
223
|
+
const directory = await mkdtemp(
|
|
224
|
+
resolve(tmpdir(), `token-ledger-import-${process.pid}-`),
|
|
225
|
+
);
|
|
63
226
|
const path = resolve(directory, "usage.sqlite");
|
|
64
227
|
const database = new DatabaseSync(path);
|
|
228
|
+
try {
|
|
229
|
+
await chmod(path, 0o600);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
try {
|
|
232
|
+
database.close();
|
|
233
|
+
} catch {
|
|
234
|
+
// The chmod failure remains authoritative.
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
await rm(directory, { recursive: true, force: true });
|
|
238
|
+
} catch {
|
|
239
|
+
// The chmod failure remains authoritative.
|
|
240
|
+
}
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
65
243
|
database.exec(`
|
|
66
244
|
PRAGMA journal_mode = OFF;
|
|
67
245
|
PRAGMA synchronous = OFF;
|
|
@@ -75,6 +253,11 @@ async function createUsageSpool() {
|
|
|
75
253
|
output_tokens REAL NOT NULL,
|
|
76
254
|
reasoning_tokens REAL NOT NULL,
|
|
77
255
|
total_tokens REAL NOT NULL,
|
|
256
|
+
tool_calls REAL NOT NULL DEFAULT 0,
|
|
257
|
+
call_count REAL NOT NULL DEFAULT 1,
|
|
258
|
+
detailed_call_count REAL NOT NULL DEFAULT 1,
|
|
259
|
+
input_call_count REAL NOT NULL DEFAULT 1,
|
|
260
|
+
components_valid INTEGER NOT NULL,
|
|
78
261
|
timestamp TEXT NOT NULL,
|
|
79
262
|
thread_id TEXT NOT NULL,
|
|
80
263
|
model TEXT NOT NULL,
|
|
@@ -83,7 +266,16 @@ async function createUsageSpool() {
|
|
|
83
266
|
git_origin TEXT,
|
|
84
267
|
raw_source TEXT,
|
|
85
268
|
service_tier TEXT,
|
|
86
|
-
original_likely INTEGER NOT NULL
|
|
269
|
+
original_likely INTEGER NOT NULL,
|
|
270
|
+
project TEXT,
|
|
271
|
+
display_model TEXT,
|
|
272
|
+
source_label TEXT,
|
|
273
|
+
use_type TEXT,
|
|
274
|
+
rate_card_model TEXT,
|
|
275
|
+
rate_card_credits REAL,
|
|
276
|
+
identity_kind TEXT NOT NULL DEFAULT 'exact',
|
|
277
|
+
range_allocation_estimated INTEGER NOT NULL DEFAULT 0,
|
|
278
|
+
range_allocation_origin TEXT
|
|
87
279
|
) WITHOUT ROWID;
|
|
88
280
|
CREATE TABLE tool_calls (
|
|
89
281
|
call_key TEXT PRIMARY KEY,
|
|
@@ -109,9 +301,19 @@ async function createUsageSpool() {
|
|
|
109
301
|
INSERT OR IGNORE INTO token_events (
|
|
110
302
|
event_key, turn_id, input_tokens, cached_input_tokens,
|
|
111
303
|
cache_write_input_tokens, output_tokens, reasoning_tokens,
|
|
112
|
-
total_tokens,
|
|
113
|
-
|
|
114
|
-
|
|
304
|
+
total_tokens, tool_calls, call_count, detailed_call_count,
|
|
305
|
+
input_call_count, components_valid, timestamp, thread_id, model, effort,
|
|
306
|
+
cwd, git_origin, raw_source, service_tier, original_likely, project,
|
|
307
|
+
display_model, source_label, use_type, rate_card_model, rate_card_credits,
|
|
308
|
+
identity_kind, range_allocation_estimated, range_allocation_origin
|
|
309
|
+
) VALUES (
|
|
310
|
+
?, ?, ?, ?, ?,
|
|
311
|
+
?, ?, ?, ?, ?,
|
|
312
|
+
?, ?, ?, ?, ?,
|
|
313
|
+
?, ?, ?, ?, ?,
|
|
314
|
+
?, ?, ?, ?, ?,
|
|
315
|
+
?, ?, ?, ?, ?, ?
|
|
316
|
+
)
|
|
115
317
|
`);
|
|
116
318
|
const promoteToken = database.prepare(`
|
|
117
319
|
UPDATE token_events
|
|
@@ -120,6 +322,28 @@ async function createUsageSpool() {
|
|
|
120
322
|
original_likely = 1
|
|
121
323
|
WHERE event_key = ? AND original_likely = 0
|
|
122
324
|
`);
|
|
325
|
+
const restoreTokenPlacement = database.prepare(`
|
|
326
|
+
UPDATE token_events
|
|
327
|
+
SET timestamp = ?, range_allocation_estimated = ?,
|
|
328
|
+
range_allocation_origin = ?
|
|
329
|
+
WHERE event_key = ?
|
|
330
|
+
`);
|
|
331
|
+
const enrichToken = database.prepare(`
|
|
332
|
+
UPDATE token_events
|
|
333
|
+
SET tool_calls = MAX(tool_calls, COALESCE(?, 0)),
|
|
334
|
+
project = COALESCE(project, ?),
|
|
335
|
+
display_model = COALESCE(display_model, ?),
|
|
336
|
+
source_label = COALESCE(source_label, ?),
|
|
337
|
+
use_type = COALESCE(use_type, ?),
|
|
338
|
+
rate_card_model = COALESCE(rate_card_model, ?),
|
|
339
|
+
rate_card_credits = COALESCE(rate_card_credits, ?),
|
|
340
|
+
identity_kind = COALESCE(identity_kind, ?),
|
|
341
|
+
range_allocation_estimated = MAX(
|
|
342
|
+
range_allocation_estimated, COALESCE(?, 0)
|
|
343
|
+
),
|
|
344
|
+
range_allocation_origin = COALESCE(range_allocation_origin, ?)
|
|
345
|
+
WHERE event_key = ?
|
|
346
|
+
`);
|
|
123
347
|
const insertCall = database.prepare(`
|
|
124
348
|
INSERT OR IGNORE INTO tool_calls (
|
|
125
349
|
call_key, turn_id, thread_id, original_likely
|
|
@@ -154,6 +378,7 @@ async function createUsageSpool() {
|
|
|
154
378
|
WHERE turn_id = ?
|
|
155
379
|
`);
|
|
156
380
|
let writing = true;
|
|
381
|
+
const activeReadStatements = new Set();
|
|
157
382
|
|
|
158
383
|
function originValues(candidate) {
|
|
159
384
|
return [
|
|
@@ -165,8 +390,8 @@ async function createUsageSpool() {
|
|
|
165
390
|
: Number.MAX_SAFE_INTEGER,
|
|
166
391
|
spoolText(candidate.model, 200),
|
|
167
392
|
spoolText(candidate.effort, 80),
|
|
168
|
-
|
|
169
|
-
|
|
393
|
+
spoolCwd(candidate.cwd),
|
|
394
|
+
spoolGitOrigin(candidate.gitOrigin),
|
|
170
395
|
spoolSource(candidate.rawSource),
|
|
171
396
|
candidate.serviceTier == null
|
|
172
397
|
? null
|
|
@@ -193,7 +418,14 @@ async function createUsageSpool() {
|
|
|
193
418
|
values[0],
|
|
194
419
|
);
|
|
195
420
|
},
|
|
196
|
-
insertToken(
|
|
421
|
+
insertToken(
|
|
422
|
+
eventKey,
|
|
423
|
+
turnId,
|
|
424
|
+
usage,
|
|
425
|
+
occurrence,
|
|
426
|
+
originalLikely,
|
|
427
|
+
metadata = {},
|
|
428
|
+
) {
|
|
197
429
|
const values = [
|
|
198
430
|
eventKey,
|
|
199
431
|
turnId,
|
|
@@ -203,17 +435,35 @@ async function createUsageSpool() {
|
|
|
203
435
|
usage.outputTokens,
|
|
204
436
|
usage.reasoningTokens,
|
|
205
437
|
usage.totalTokens,
|
|
438
|
+
usage.toolCalls ?? 0,
|
|
439
|
+
metadata.callCount ?? 1,
|
|
440
|
+
metadata.detailedCallCount ?? (
|
|
441
|
+
hasDetailedTokenBreakdown(usage) ? 1 : 0
|
|
442
|
+
),
|
|
443
|
+
metadata.inputCallCount ?? (usage.inputTokens > 0 ? 1 : 0),
|
|
444
|
+
usage.componentsValid ? 1 : 0,
|
|
206
445
|
occurrence.timestamp,
|
|
207
446
|
occurrence.threadId,
|
|
208
447
|
spoolText(occurrence.model, 200),
|
|
209
448
|
spoolText(occurrence.effort, 80),
|
|
210
|
-
|
|
211
|
-
|
|
449
|
+
spoolCwd(occurrence.cwd),
|
|
450
|
+
spoolGitOrigin(occurrence.gitOrigin),
|
|
212
451
|
spoolSource(occurrence.rawSource),
|
|
213
452
|
occurrence.serviceTier == null
|
|
214
453
|
? null
|
|
215
454
|
: spoolText(occurrence.serviceTier, 80),
|
|
216
455
|
originalLikely ? 1 : 0,
|
|
456
|
+
metadata.project ?? null,
|
|
457
|
+
metadata.displayModel ?? null,
|
|
458
|
+
metadata.source ?? null,
|
|
459
|
+
metadata.useType ?? null,
|
|
460
|
+
metadata.rateCardModel ?? null,
|
|
461
|
+
metadata.rateCardCredits ?? null,
|
|
462
|
+
metadata.identityKind ?? "exact",
|
|
463
|
+
metadata.rangeAllocationEstimated ? 1 : 0,
|
|
464
|
+
metadata.rangeAllocationOrigin == null
|
|
465
|
+
? null
|
|
466
|
+
: JSON.stringify(metadata.rangeAllocationOrigin),
|
|
217
467
|
];
|
|
218
468
|
const inserted = insertToken.run(...values).changes > 0;
|
|
219
469
|
if (!inserted && originalLikely) {
|
|
@@ -223,8 +473,8 @@ async function createUsageSpool() {
|
|
|
223
473
|
occurrence.threadId,
|
|
224
474
|
spoolText(occurrence.model, 200),
|
|
225
475
|
spoolText(occurrence.effort, 80),
|
|
226
|
-
|
|
227
|
-
|
|
476
|
+
spoolCwd(occurrence.cwd),
|
|
477
|
+
spoolGitOrigin(occurrence.gitOrigin),
|
|
228
478
|
spoolSource(occurrence.rawSource),
|
|
229
479
|
occurrence.serviceTier == null
|
|
230
480
|
? null
|
|
@@ -232,8 +482,35 @@ async function createUsageSpool() {
|
|
|
232
482
|
eventKey,
|
|
233
483
|
);
|
|
234
484
|
}
|
|
485
|
+
if (!inserted && metadata && Object.keys(metadata).length > 0) {
|
|
486
|
+
enrichToken.run(
|
|
487
|
+
usage.toolCalls ?? 0,
|
|
488
|
+
metadata.project ?? null,
|
|
489
|
+
metadata.displayModel ?? null,
|
|
490
|
+
metadata.source ?? null,
|
|
491
|
+
metadata.useType ?? null,
|
|
492
|
+
metadata.rateCardModel ?? null,
|
|
493
|
+
metadata.rateCardCredits ?? null,
|
|
494
|
+
metadata.identityKind ?? null,
|
|
495
|
+
metadata.rangeAllocationEstimated ? 1 : 0,
|
|
496
|
+
metadata.rangeAllocationOrigin == null
|
|
497
|
+
? null
|
|
498
|
+
: JSON.stringify(metadata.rangeAllocationOrigin),
|
|
499
|
+
eventKey,
|
|
500
|
+
);
|
|
501
|
+
}
|
|
235
502
|
return inserted;
|
|
236
503
|
},
|
|
504
|
+
restoreTokenPlacement(eventKey, occurrence, metadata = {}) {
|
|
505
|
+
return restoreTokenPlacement.run(
|
|
506
|
+
occurrence.timestamp,
|
|
507
|
+
metadata.rangeAllocationEstimated ? 1 : 0,
|
|
508
|
+
metadata.rangeAllocationOrigin == null
|
|
509
|
+
? null
|
|
510
|
+
: JSON.stringify(metadata.rangeAllocationOrigin),
|
|
511
|
+
eventKey,
|
|
512
|
+
).changes > 0;
|
|
513
|
+
},
|
|
237
514
|
insertCall(callKey, turnId, threadId, originalLikely) {
|
|
238
515
|
const inserted = insertCall.run(
|
|
239
516
|
callKey,
|
|
@@ -250,8 +527,11 @@ async function createUsageSpool() {
|
|
|
250
527
|
database.exec("COMMIT");
|
|
251
528
|
writing = false;
|
|
252
529
|
},
|
|
253
|
-
tokenRows() {
|
|
254
|
-
|
|
530
|
+
*tokenRows() {
|
|
531
|
+
// Node 22 does not keep StatementSync alive through a bare iterator.
|
|
532
|
+
// Keep an explicit heap reference until iteration has completed; a
|
|
533
|
+
// generator-local reference alone can still be finalized on long scans.
|
|
534
|
+
const statement = database.prepare(`
|
|
255
535
|
SELECT token.event_key AS eventKey, token.turn_id AS turnId,
|
|
256
536
|
token.input_tokens AS inputTokens,
|
|
257
537
|
token.cached_input_tokens AS cachedInputTokens,
|
|
@@ -259,12 +539,26 @@ async function createUsageSpool() {
|
|
|
259
539
|
token.output_tokens AS outputTokens,
|
|
260
540
|
token.reasoning_tokens AS reasoningTokens,
|
|
261
541
|
token.total_tokens AS totalTokens,
|
|
542
|
+
token.tool_calls AS toolCalls,
|
|
543
|
+
token.call_count AS callCount,
|
|
544
|
+
token.detailed_call_count AS detailedCallCount,
|
|
545
|
+
token.input_call_count AS inputCallCount,
|
|
546
|
+
token.components_valid AS componentsValid,
|
|
262
547
|
token.timestamp, token.thread_id AS threadId,
|
|
263
548
|
token.model, token.effort, token.cwd,
|
|
264
549
|
token.git_origin AS gitOrigin,
|
|
265
550
|
token.raw_source AS rawSource,
|
|
266
551
|
token.service_tier AS serviceTier,
|
|
267
552
|
token.original_likely AS originalLikely,
|
|
553
|
+
token.project,
|
|
554
|
+
token.display_model AS displayModel,
|
|
555
|
+
token.source_label AS sourceLabel,
|
|
556
|
+
token.use_type AS useType,
|
|
557
|
+
token.rate_card_model AS rateCardModel,
|
|
558
|
+
token.rate_card_credits AS rateCardCredits,
|
|
559
|
+
token.identity_kind AS identityKind,
|
|
560
|
+
token.range_allocation_estimated AS rangeAllocationEstimated,
|
|
561
|
+
token.range_allocation_origin AS rangeAllocationOrigin,
|
|
268
562
|
origin.thread_id AS originThreadId,
|
|
269
563
|
origin.timestamp AS originTimestamp,
|
|
270
564
|
origin.model AS originModel,
|
|
@@ -276,15 +570,31 @@ async function createUsageSpool() {
|
|
|
276
570
|
FROM token_events AS token
|
|
277
571
|
LEFT JOIN turn_origins AS origin ON origin.turn_id = token.turn_id
|
|
278
572
|
ORDER BY token.timestamp, token.event_key
|
|
279
|
-
`)
|
|
573
|
+
`);
|
|
574
|
+
activeReadStatements.add(statement);
|
|
575
|
+
try {
|
|
576
|
+
for (const row of statement.iterate()) {
|
|
577
|
+
yield repairTokenTimestamp(row);
|
|
578
|
+
}
|
|
579
|
+
} finally {
|
|
580
|
+
activeReadStatements.delete(statement);
|
|
581
|
+
}
|
|
280
582
|
},
|
|
281
|
-
callRows() {
|
|
282
|
-
|
|
283
|
-
SELECT call.
|
|
583
|
+
*callRows() {
|
|
584
|
+
const statement = database.prepare(`
|
|
585
|
+
SELECT call.call_key AS callKey,
|
|
586
|
+
call.turn_id AS turnId,
|
|
587
|
+
call.original_likely AS originalLikely,
|
|
284
588
|
COALESCE(origin.thread_id, call.thread_id) AS threadId
|
|
285
589
|
FROM tool_calls AS call
|
|
286
590
|
LEFT JOIN turn_origins AS origin ON origin.turn_id = call.turn_id
|
|
287
|
-
`)
|
|
591
|
+
`);
|
|
592
|
+
activeReadStatements.add(statement);
|
|
593
|
+
try {
|
|
594
|
+
yield* statement.iterate();
|
|
595
|
+
} finally {
|
|
596
|
+
activeReadStatements.delete(statement);
|
|
597
|
+
}
|
|
288
598
|
},
|
|
289
599
|
close() {
|
|
290
600
|
if (writing) {
|
|
@@ -300,18 +610,6 @@ async function createUsageSpool() {
|
|
|
300
610
|
};
|
|
301
611
|
}
|
|
302
612
|
|
|
303
|
-
const RATE_CARD = {
|
|
304
|
-
"gpt-5.6-sol": { input: 125, cached: 12.5, output: 750 },
|
|
305
|
-
"gpt-5.6-terra": { input: 50, cached: 5, output: 300 },
|
|
306
|
-
"gpt-5.6-luna": { input: 5, cached: 0.5, output: 30 },
|
|
307
|
-
"gpt-5.5": { input: 125, cached: 12.5, output: 750 },
|
|
308
|
-
"gpt-5.5-cyber": { input: 500, cached: 50, output: 3_000 },
|
|
309
|
-
"gpt-5.4": { input: 62.5, cached: 6.25, output: 375 },
|
|
310
|
-
"gpt-5.4-mini": { input: 18.75, cached: 1.875, output: 113 },
|
|
311
|
-
"gpt-5.3-codex": { input: 43.75, cached: 4.375, output: 350 },
|
|
312
|
-
"gpt-5.2": { input: 43.75, cached: 4.375, output: 350 },
|
|
313
|
-
};
|
|
314
|
-
|
|
315
613
|
function usage() {
|
|
316
614
|
return `Token Ledger local collector
|
|
317
615
|
|
|
@@ -319,21 +617,25 @@ Usage:
|
|
|
319
617
|
node token-ledger-importer.mjs [options]
|
|
320
618
|
|
|
321
619
|
Options:
|
|
322
|
-
--output <file> Snapshot destination (default: token-ledger-snapshot-
|
|
620
|
+
--output <file> Snapshot destination (default: token-ledger-snapshot-v3.json.gz)
|
|
323
621
|
--codex-home <dir> Codex data root (default: CODEX_HOME or ~/.codex)
|
|
324
|
-
--since <ISO
|
|
622
|
+
--since <ISO timestamp> Ignore model calls before this timestamp
|
|
325
623
|
--no-archived Skip archived_sessions
|
|
326
624
|
--help Show this help
|
|
327
625
|
|
|
328
626
|
The snapshot contains usage metadata and Codex display titles only. It never
|
|
329
627
|
contains message bodies, tool payloads, reasoning text, credential fields, or
|
|
330
|
-
full local paths in its output.
|
|
331
|
-
|
|
628
|
+
full local paths in its output. Path-like source labels are categorized, and
|
|
629
|
+
local path tokens in other labels are redacted. Codex display titles may still
|
|
630
|
+
contain unrelated user-written text. Refreshes also update the app-owned
|
|
631
|
+
durable ledger in ~/.token-ledger. Selecting another snapshot output never
|
|
632
|
+
relocates SQLite state.
|
|
633
|
+
The ledger retains committed observations when source files later disappear.`;
|
|
332
634
|
}
|
|
333
635
|
|
|
334
636
|
function parseArgs(argv) {
|
|
335
637
|
const options = {
|
|
336
|
-
output: resolve("token-ledger-snapshot-
|
|
638
|
+
output: resolve("token-ledger-snapshot-v3.json.gz"),
|
|
337
639
|
codexHome: resolve(process.env.CODEX_HOME || `${homedir()}/.codex`),
|
|
338
640
|
includeArchived: true,
|
|
339
641
|
since: null,
|
|
@@ -355,10 +657,8 @@ function parseArgs(argv) {
|
|
|
355
657
|
options.codexHome = resolve(value);
|
|
356
658
|
} else if (argument === "--since") {
|
|
357
659
|
const value = argv[++index];
|
|
358
|
-
if (!value
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
options.since = new Date(value);
|
|
660
|
+
if (!value) throw new Error("--since requires a valid ISO timestamp.");
|
|
661
|
+
options.since = new Date(normalizeCollectionSince(value));
|
|
362
662
|
} else {
|
|
363
663
|
throw new Error(`Unknown option: ${argument}`);
|
|
364
664
|
}
|
|
@@ -389,9 +689,10 @@ function safeIso(value, fallback = null) {
|
|
|
389
689
|
}
|
|
390
690
|
|
|
391
691
|
function tokenTuple(value) {
|
|
392
|
-
// token_count payloads arrive from untrusted JSONL.
|
|
393
|
-
//
|
|
394
|
-
//
|
|
692
|
+
// token_count payloads arrive from untrusted JSONL. Token counts are kept
|
|
693
|
+
// only when they are primitive, non-negative safe integers. Invalid
|
|
694
|
+
// components become unknown breakdown values; the total is validated by the
|
|
695
|
+
// caller before the record enters the usage spool.
|
|
395
696
|
const {
|
|
396
697
|
input_tokens: inputTokens,
|
|
397
698
|
cached_input_tokens: cachedInputTokens,
|
|
@@ -400,76 +701,40 @@ function tokenTuple(value) {
|
|
|
400
701
|
reasoning_output_tokens: reasoningOutputTokens,
|
|
401
702
|
total_tokens: totalTokens,
|
|
402
703
|
} = value ?? {};
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
704
|
+
const rawValues = [
|
|
705
|
+
inputTokens,
|
|
706
|
+
cachedInputTokens,
|
|
707
|
+
cacheWriteInputTokens,
|
|
708
|
+
outputTokens,
|
|
709
|
+
reasoningOutputTokens,
|
|
710
|
+
totalTokens,
|
|
410
711
|
];
|
|
712
|
+
return {
|
|
713
|
+
values: rawValues.map((value) => tokenValue(value)),
|
|
714
|
+
valid: rawValues.map((value, index) =>
|
|
715
|
+
index < 5 && value === undefined
|
|
716
|
+
? true
|
|
717
|
+
: isValidTokenValue(value)),
|
|
718
|
+
};
|
|
411
719
|
}
|
|
412
720
|
|
|
413
721
|
function usageFromTuple(tuple) {
|
|
414
722
|
// Cached input is a subset of input and reasoning is a subset of output.
|
|
415
723
|
// Clamp at export so one out-of-range source record cannot skew subset
|
|
416
724
|
// math downstream.
|
|
417
|
-
const inputTokens = tuple[0];
|
|
418
|
-
const outputTokens = tuple[3];
|
|
725
|
+
const inputTokens = tuple.values[0];
|
|
726
|
+
const outputTokens = tuple.values[3];
|
|
419
727
|
return {
|
|
420
728
|
inputTokens,
|
|
421
|
-
cachedInputTokens: Math.min(inputTokens, tuple[1]),
|
|
422
|
-
cacheWriteInputTokens: tuple[2],
|
|
729
|
+
cachedInputTokens: Math.min(inputTokens, tuple.values[1]),
|
|
730
|
+
cacheWriteInputTokens: tuple.values[2],
|
|
423
731
|
outputTokens,
|
|
424
|
-
reasoningTokens: Math.min(outputTokens, tuple[4]),
|
|
425
|
-
totalTokens: tuple[5],
|
|
732
|
+
reasoningTokens: Math.min(outputTokens, tuple.values[4]),
|
|
733
|
+
totalTokens: tuple.values[5],
|
|
734
|
+
componentsValid: tuple.valid.slice(0, -1).every(Boolean),
|
|
426
735
|
};
|
|
427
736
|
}
|
|
428
737
|
|
|
429
|
-
function hasDetailedBreakdown(usage) {
|
|
430
|
-
if (usage.totalTokens === 0) return true;
|
|
431
|
-
return (
|
|
432
|
-
usage.inputTokens + usage.outputTokens === usage.totalTokens &&
|
|
433
|
-
(usage.inputTokens > 0 || usage.outputTokens > 0)
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
|
|
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.
|
|
442
|
-
const value = String(model || "unknown")
|
|
443
|
-
.trim()
|
|
444
|
-
.toLowerCase()
|
|
445
|
-
.replace(/[\s_]+/g, "-");
|
|
446
|
-
if (RATE_CARD[value]) return value;
|
|
447
|
-
if (value.startsWith("gpt-5.6-sol")) return "gpt-5.6-sol";
|
|
448
|
-
if (value.startsWith("gpt-5.6-terra")) return "gpt-5.6-terra";
|
|
449
|
-
if (value.startsWith("gpt-5.6-luna")) return "gpt-5.6-luna";
|
|
450
|
-
if (value.startsWith("gpt-5.5-cyber")) return "gpt-5.5-cyber";
|
|
451
|
-
if (value.startsWith("gpt-5.5")) return "gpt-5.5";
|
|
452
|
-
if (value.startsWith("gpt-5.4-mini")) return "gpt-5.4-mini";
|
|
453
|
-
if (value.startsWith("gpt-5.4")) return "gpt-5.4";
|
|
454
|
-
if (value.startsWith("gpt-5.3-codex")) return "gpt-5.3-codex";
|
|
455
|
-
if (value.startsWith("gpt-5.2")) return "gpt-5.2";
|
|
456
|
-
return value || "unknown";
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function creditsForUsage(model, usage) {
|
|
460
|
-
if (!hasDetailedBreakdown(usage)) return null;
|
|
461
|
-
const rate = RATE_CARD[normalizeModel(model)];
|
|
462
|
-
if (!rate) return null;
|
|
463
|
-
const cached = Math.min(usage.inputTokens, usage.cachedInputTokens);
|
|
464
|
-
const uncached = Math.max(0, usage.inputTokens - cached);
|
|
465
|
-
return (
|
|
466
|
-
(uncached * rate.input +
|
|
467
|
-
cached * rate.cached +
|
|
468
|
-
usage.outputTokens * rate.output) /
|
|
469
|
-
1_000_000
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
738
|
function primitiveString(value) {
|
|
474
739
|
try {
|
|
475
740
|
const text = String.prototype.valueOf.call(value);
|
|
@@ -479,6 +744,12 @@ function primitiveString(value) {
|
|
|
479
744
|
}
|
|
480
745
|
}
|
|
481
746
|
|
|
747
|
+
function liveRecordTimestamp(value) {
|
|
748
|
+
const source = primitiveString(value);
|
|
749
|
+
if (source === null || source.length === 0) return null;
|
|
750
|
+
return safeIso(source, null);
|
|
751
|
+
}
|
|
752
|
+
|
|
482
753
|
function sourceDescriptor(value) {
|
|
483
754
|
// Thread sources reach us in two representations: rollout session_meta
|
|
484
755
|
// lines carry parsed JSON (a label string or an object with subagent
|
|
@@ -503,6 +774,9 @@ function sourceDescriptor(value) {
|
|
|
503
774
|
|
|
504
775
|
function sourceLabels(threadSource, rawSource) {
|
|
505
776
|
const source = sourceDescriptor(rawSource);
|
|
777
|
+
const safeThreadSource = safeExportLabel(threadSource, 40, "");
|
|
778
|
+
const localThreadSource = containsLocalPath(threadSource);
|
|
779
|
+
const localRawSource = containsLocalPath(source.label);
|
|
506
780
|
if (threadSource === "subagent" || source.subagent) {
|
|
507
781
|
return { source: "subagent", useType: "subagent" };
|
|
508
782
|
}
|
|
@@ -516,13 +790,21 @@ function sourceLabels(threadSource, rawSource) {
|
|
|
516
790
|
if (source.label === "vscode") {
|
|
517
791
|
return { source: "desktop", useType: "interactive" };
|
|
518
792
|
}
|
|
793
|
+
if (localThreadSource || localRawSource) {
|
|
794
|
+
return {
|
|
795
|
+
source: LOCAL_LABEL,
|
|
796
|
+
useType: localThreadSource
|
|
797
|
+
? LOCAL_LABEL
|
|
798
|
+
: safeThreadSource || "interactive",
|
|
799
|
+
};
|
|
800
|
+
}
|
|
519
801
|
if (source.label) {
|
|
520
802
|
return {
|
|
521
|
-
source: source.label
|
|
522
|
-
useType:
|
|
803
|
+
source: safeExportLabel(source.label, 40, "unknown"),
|
|
804
|
+
useType: safeThreadSource || "interactive",
|
|
523
805
|
};
|
|
524
806
|
}
|
|
525
|
-
return { source: "unknown", useType:
|
|
807
|
+
return { source: "unknown", useType: safeThreadSource || "unknown" };
|
|
526
808
|
}
|
|
527
809
|
|
|
528
810
|
function cleanRemote(value) {
|
|
@@ -542,17 +824,40 @@ function cleanRemote(value) {
|
|
|
542
824
|
}
|
|
543
825
|
}
|
|
544
826
|
|
|
827
|
+
function isRemoteOrigin(value) {
|
|
828
|
+
const remote = spoolText(value).trim();
|
|
829
|
+
return (
|
|
830
|
+
/^(?:https?|git|ssh|git\+ssh):\/\//i.test(remote) ||
|
|
831
|
+
/^[^@\s/:]+@[^:\s/]+:.+$/.test(remote)
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
545
835
|
function projectLabel(cwd, gitOrigin) {
|
|
546
836
|
const remote = cleanRemote(gitOrigin);
|
|
837
|
+
if (remote && isRemoteOrigin(gitOrigin)) {
|
|
838
|
+
const parts = remote.split("/").filter(Boolean);
|
|
839
|
+
return safeExportLabel(
|
|
840
|
+
parts.slice(-2).join("/"),
|
|
841
|
+
160,
|
|
842
|
+
"Unknown project",
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
if (containsLocalPath(gitOrigin)) return LOCAL_LABEL;
|
|
547
846
|
if (remote) {
|
|
548
847
|
const parts = remote.split("/").filter(Boolean);
|
|
549
|
-
return
|
|
848
|
+
return safeExportLabel(
|
|
849
|
+
parts.slice(-2).join("/"),
|
|
850
|
+
160,
|
|
851
|
+
"Unknown project",
|
|
852
|
+
);
|
|
550
853
|
}
|
|
551
|
-
const path =
|
|
854
|
+
const path = spoolText(cwd).replaceAll("\\", "/").replace(/\/+$/, "");
|
|
552
855
|
const worktree = path.match(/\/\.codex\/worktrees\/[^/]+\/([^/]+)$/);
|
|
553
|
-
if (worktree) return worktree[1];
|
|
856
|
+
if (worktree) return safeExportLabel(worktree[1], 160, "Unknown project");
|
|
554
857
|
const name = basename(path);
|
|
555
|
-
return name && name !== "." && name !== "/"
|
|
858
|
+
return name && name !== "." && name !== "/"
|
|
859
|
+
? safeExportLabel(name, 160, "Unknown project")
|
|
860
|
+
: "Unknown project";
|
|
556
861
|
}
|
|
557
862
|
|
|
558
863
|
function safeTitle(row, sessionTitle) {
|
|
@@ -565,18 +870,16 @@ function safeTitle(row, sessionTitle) {
|
|
|
565
870
|
candidate.includes("<codex_delegation>") ||
|
|
566
871
|
(subagent && candidate.length > 120)
|
|
567
872
|
) {
|
|
568
|
-
if (row?.agent_nickname)
|
|
873
|
+
if (row?.agent_nickname) {
|
|
874
|
+
return safeExportLabel(
|
|
875
|
+
`Subagent · ${row.agent_nickname}`,
|
|
876
|
+
180,
|
|
877
|
+
"Subagent",
|
|
878
|
+
);
|
|
879
|
+
}
|
|
569
880
|
return subagent ? `Subagent · ${String(row?.id || "").slice(0, 8)}` : "Untitled task";
|
|
570
881
|
}
|
|
571
|
-
return candidate
|
|
572
|
-
.replace(/\/Users\/[^\s"'`]+/g, "[local path]")
|
|
573
|
-
.replace(/\/(?:private\/)?tmp\/[^\s"'`]+/g, "[temporary path]")
|
|
574
|
-
.replace(
|
|
575
|
-
/\b(?:sk-[A-Za-z0-9_-]{16,}|lin_api_[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_-]{16,})\b/g,
|
|
576
|
-
"[redacted credential-like text]",
|
|
577
|
-
)
|
|
578
|
-
.replace(/\s+/g, " ")
|
|
579
|
-
.slice(0, 180);
|
|
882
|
+
return safeExportLabel(candidate, 180, "Untitled task");
|
|
580
883
|
}
|
|
581
884
|
|
|
582
885
|
async function pathExists(path) {
|
|
@@ -604,26 +907,425 @@ export async function listJsonlFiles(root) {
|
|
|
604
907
|
return found;
|
|
605
908
|
}
|
|
606
909
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
910
|
+
function sourceManifestEntry(codexHome, path, sourceStat) {
|
|
911
|
+
return [
|
|
912
|
+
hash(relative(resolve(codexHome), path), 64),
|
|
913
|
+
sourceStat.size,
|
|
914
|
+
sourceStat.mtimeMs,
|
|
915
|
+
sourceStat.ctimeMs,
|
|
916
|
+
sourceStat.dev ?? null,
|
|
917
|
+
sourceStat.ino ?? null,
|
|
918
|
+
];
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function normalizeSourcePath(value) {
|
|
922
|
+
const normalized = String(value).replaceAll("\\", "/");
|
|
923
|
+
return normalized.replace(/\/{2,}/g, "/").replace(/\/$/, "") || "/";
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
export function sourceLocationForPath(codexHome, path) {
|
|
927
|
+
const root = normalizeSourcePath(codexHome);
|
|
928
|
+
const candidate = normalizeSourcePath(path);
|
|
929
|
+
const archiveRoot = `${root}/archived_sessions`;
|
|
930
|
+
return candidate === archiveRoot || candidate.startsWith(`${archiveRoot}/`)
|
|
931
|
+
? "archived"
|
|
932
|
+
: "active";
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function sourceIdsForRolloutPaths(codexHome, entries) {
|
|
936
|
+
const root = resolve(codexHome);
|
|
937
|
+
const sourceIds = new Map();
|
|
938
|
+
for (const { path, sourceStat } of entries) {
|
|
939
|
+
const baseId = durableSourceId(root, path);
|
|
940
|
+
const fileIdentity = sourceStat.dev != null && sourceStat.ino != null
|
|
941
|
+
? `inode:${sourceStat.dev}:${sourceStat.ino}`
|
|
942
|
+
: `path:${relative(root, path)}`;
|
|
943
|
+
sourceIds.set(path, `${baseId}:file:${hash(fileIdentity, 16)}`);
|
|
611
944
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
945
|
+
return sourceIds;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function sourceWatermarkFromEntries(entries) {
|
|
949
|
+
const serialized = JSON.stringify(entries);
|
|
950
|
+
return {
|
|
951
|
+
version: SOURCE_WATERMARK_VERSION,
|
|
952
|
+
fingerprint: hash(serialized, 64),
|
|
953
|
+
sourceCount: entries.length,
|
|
954
|
+
latestModifiedAt: entries.reduce(
|
|
955
|
+
(latest, entry) => Math.max(latest, entry[2]),
|
|
956
|
+
0,
|
|
957
|
+
),
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
export function sourceWatermarksEqual(left, right) {
|
|
962
|
+
return Boolean(
|
|
963
|
+
left &&
|
|
964
|
+
right &&
|
|
965
|
+
left.version === SOURCE_WATERMARK_VERSION &&
|
|
966
|
+
right.version === SOURCE_WATERMARK_VERSION &&
|
|
967
|
+
left.fingerprint === right.fingerprint &&
|
|
968
|
+
left.sourceCount === right.sourceCount &&
|
|
969
|
+
left.latestModifiedAt === right.latestModifiedAt,
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
async function prefixFingerprint(path, byteLength) {
|
|
974
|
+
const fingerprint = createHash("sha256");
|
|
975
|
+
if (byteLength === 0) return fingerprint.digest("hex");
|
|
976
|
+
const input = createReadStream(path, {
|
|
977
|
+
start: 0,
|
|
978
|
+
end: byteLength - 1,
|
|
979
|
+
});
|
|
980
|
+
for await (const chunk of input) fingerprint.update(chunk);
|
|
981
|
+
return fingerprint.digest("hex");
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
async function descriptorPrefixFingerprint(handle, byteLength) {
|
|
985
|
+
const fingerprint = createHash("sha256");
|
|
986
|
+
let offset = 0;
|
|
987
|
+
while (offset < byteLength) {
|
|
988
|
+
const chunkLength = Math.min(MAX_TRAILING_SCAN_BYTES, byteLength - offset);
|
|
989
|
+
const chunk = Buffer.allocUnsafe(chunkLength);
|
|
990
|
+
const { bytesRead } = await handle.read(
|
|
991
|
+
chunk,
|
|
992
|
+
0,
|
|
993
|
+
chunkLength,
|
|
994
|
+
offset,
|
|
995
|
+
);
|
|
996
|
+
if (bytesRead !== chunkLength) return null;
|
|
997
|
+
fingerprint.update(chunk.subarray(0, bytesRead));
|
|
998
|
+
offset += bytesRead;
|
|
999
|
+
}
|
|
1000
|
+
return fingerprint.digest("hex");
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function sourceIdentityChanged(sourceStat, expectedIdentity) {
|
|
1004
|
+
return expectedIdentity?.device != null &&
|
|
1005
|
+
expectedIdentity?.inode != null &&
|
|
1006
|
+
(
|
|
1007
|
+
Number(sourceStat.dev) !== Number(expectedIdentity.device) ||
|
|
1008
|
+
Number(sourceStat.ino) !== Number(expectedIdentity.inode)
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function sourceIdentityDiffers(left, right) {
|
|
1013
|
+
const leftHasIdentity = left?.dev != null && left?.ino != null;
|
|
1014
|
+
const rightHasIdentity = right?.dev != null && right?.ino != null;
|
|
1015
|
+
return leftHasIdentity !== rightHasIdentity ||
|
|
1016
|
+
(leftHasIdentity &&
|
|
1017
|
+
(
|
|
1018
|
+
Number(left.dev) !== Number(right.dev) ||
|
|
1019
|
+
Number(left.ino) !== Number(right.ino)
|
|
1020
|
+
));
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function sourceRevisionChanged(left, right) {
|
|
1024
|
+
return (
|
|
1025
|
+
Number(left.size) !== Number(right.size) ||
|
|
1026
|
+
Number(left.mtimeMs) !== Number(right.mtimeMs) ||
|
|
1027
|
+
Number(left.ctimeMs) !== Number(right.ctimeMs) ||
|
|
1028
|
+
sourceIdentityDiffers(left, right)
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
async function revalidateScannedRollout(
|
|
1033
|
+
path,
|
|
1034
|
+
handle,
|
|
1035
|
+
opened,
|
|
1036
|
+
expectedIdentity,
|
|
1037
|
+
maximumBytes,
|
|
1038
|
+
cursorFingerprint,
|
|
1039
|
+
) {
|
|
1040
|
+
const bounded = Number.isSafeInteger(maximumBytes) && maximumBytes >= 0;
|
|
1041
|
+
const descriptor = await handle.stat();
|
|
1042
|
+
if (bounded && Number(descriptor.size) < maximumBytes) {
|
|
1043
|
+
const error = new Error(
|
|
1044
|
+
"Rollout became shorter while its bounded prefix was being read.",
|
|
1045
|
+
);
|
|
1046
|
+
error.code = "ERR_SOURCE_SIZE_CHANGED";
|
|
1047
|
+
throw error;
|
|
1048
|
+
}
|
|
1049
|
+
if (
|
|
1050
|
+
bounded &&
|
|
1051
|
+
Number(descriptor.size) === Number(opened.size) &&
|
|
1052
|
+
(
|
|
1053
|
+
Number(descriptor.mtimeMs) !== Number(opened.mtimeMs) ||
|
|
1054
|
+
Number(descriptor.ctimeMs) !== Number(opened.ctimeMs)
|
|
1055
|
+
)
|
|
1056
|
+
) {
|
|
1057
|
+
const error = new Error(
|
|
1058
|
+
"Rollout revision changed while its bounded prefix was being read.",
|
|
1059
|
+
);
|
|
1060
|
+
error.code = "ERR_SOURCE_CONTENT_CHANGED";
|
|
1061
|
+
throw error;
|
|
1062
|
+
}
|
|
1063
|
+
if (sourceIdentityChanged(descriptor, expectedIdentity)) {
|
|
1064
|
+
const error = new Error(
|
|
1065
|
+
"Rollout identity changed while its bounded prefix was being read.",
|
|
1066
|
+
);
|
|
1067
|
+
error.code = "ERR_SOURCE_IDENTITY_CHANGED";
|
|
1068
|
+
throw error;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const pathBefore = await stat(path);
|
|
1072
|
+
if (
|
|
1073
|
+
sourceIdentityChanged(pathBefore, expectedIdentity) ||
|
|
1074
|
+
sourceIdentityDiffers(pathBefore, opened)
|
|
1075
|
+
) {
|
|
1076
|
+
const error = new Error(
|
|
1077
|
+
"Rollout identity changed while its bounded prefix was being read.",
|
|
1078
|
+
);
|
|
1079
|
+
error.code = "ERR_SOURCE_IDENTITY_CHANGED";
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (bounded && Number(pathBefore.size) < maximumBytes) {
|
|
1084
|
+
const error = new Error(
|
|
1085
|
+
"Rollout became shorter while its bounded prefix was being read.",
|
|
1086
|
+
);
|
|
1087
|
+
error.code = "ERR_SOURCE_SIZE_CHANGED";
|
|
1088
|
+
throw error;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const verifyPrefix = async () => {
|
|
1092
|
+
const rereadFingerprint = await descriptorPrefixFingerprint(
|
|
1093
|
+
handle,
|
|
1094
|
+
maximumBytes,
|
|
1095
|
+
);
|
|
1096
|
+
if (rereadFingerprint !== cursorFingerprint) {
|
|
1097
|
+
const error = new Error(
|
|
1098
|
+
"Rollout content changed while its bounded prefix was being read.",
|
|
1099
|
+
);
|
|
1100
|
+
error.code = "ERR_SOURCE_CONTENT_CHANGED";
|
|
1101
|
+
throw error;
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
// The streaming scan already hashed this exact prefix. Re-read it only if
|
|
1106
|
+
// the descriptor or path stat changed while the scan was in flight; stable
|
|
1107
|
+
// size, timestamps, and identity are the normal case and need no second
|
|
1108
|
+
// pass over the file.
|
|
1109
|
+
if (
|
|
1110
|
+
bounded &&
|
|
1111
|
+
(
|
|
1112
|
+
sourceRevisionChanged(descriptor, opened) ||
|
|
1113
|
+
sourceRevisionChanged(pathBefore, opened)
|
|
1114
|
+
)
|
|
1115
|
+
) {
|
|
1116
|
+
await verifyPrefix();
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const pathAfter = await stat(path);
|
|
1120
|
+
if (
|
|
1121
|
+
sourceIdentityChanged(pathAfter, expectedIdentity) ||
|
|
1122
|
+
sourceIdentityDiffers(pathAfter, opened)
|
|
1123
|
+
) {
|
|
1124
|
+
const error = new Error(
|
|
1125
|
+
"Rollout identity changed while its bounded prefix was being read.",
|
|
1126
|
+
);
|
|
1127
|
+
error.code = "ERR_SOURCE_IDENTITY_CHANGED";
|
|
1128
|
+
throw error;
|
|
1129
|
+
}
|
|
1130
|
+
if (bounded && Number(pathAfter.size) < maximumBytes) {
|
|
1131
|
+
const error = new Error(
|
|
1132
|
+
"Rollout became shorter while its bounded prefix was being read.",
|
|
1133
|
+
);
|
|
1134
|
+
error.code = "ERR_SOURCE_SIZE_CHANGED";
|
|
1135
|
+
throw error;
|
|
1136
|
+
}
|
|
1137
|
+
if (bounded && sourceRevisionChanged(pathAfter, pathBefore)) {
|
|
1138
|
+
await verifyPrefix();
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
async function scanCutoffForFile(entry) {
|
|
1143
|
+
const size = Number(entry.size);
|
|
1144
|
+
if (!Number.isSafeInteger(size) || size <= 0) return Math.max(0, size || 0);
|
|
1145
|
+
|
|
1146
|
+
const handle = await open(entry.path, "r");
|
|
1147
|
+
try {
|
|
1148
|
+
// Start with the bounded trailing window, then continue backwards if an
|
|
1149
|
+
// unterminated record is larger than that window. A zero cutoff would
|
|
1150
|
+
// discard every complete record before the oversized partial record.
|
|
1151
|
+
let chunkEnd = size;
|
|
1152
|
+
while (chunkEnd > 0) {
|
|
1153
|
+
const chunkLength = Math.min(chunkEnd, MAX_TRAILING_SCAN_BYTES);
|
|
1154
|
+
const chunk = Buffer.allocUnsafe(chunkLength);
|
|
1155
|
+
const chunkStart = chunkEnd - chunkLength;
|
|
1156
|
+
const { bytesRead } = await handle.read(
|
|
1157
|
+
chunk,
|
|
1158
|
+
0,
|
|
1159
|
+
chunkLength,
|
|
1160
|
+
chunkStart,
|
|
1161
|
+
);
|
|
1162
|
+
if (bytesRead <= 0) return 0;
|
|
1163
|
+
const lastNewline = chunk.subarray(0, bytesRead).lastIndexOf(0x0a);
|
|
1164
|
+
if (lastNewline >= 0) return chunkStart + lastNewline + 1;
|
|
1165
|
+
chunkEnd = chunkStart;
|
|
1166
|
+
}
|
|
1167
|
+
return 0;
|
|
1168
|
+
} finally {
|
|
1169
|
+
await handle.close();
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
async function sourceInventoryPreservesCutoff(before, after) {
|
|
1174
|
+
const initial = new Map(
|
|
1175
|
+
before.files.map((entry) => [String(entry.sourceId), entry]),
|
|
1176
|
+
);
|
|
1177
|
+
const current = new Map(
|
|
1178
|
+
after.files.map((entry) => [String(entry.sourceId), entry]),
|
|
1179
|
+
);
|
|
1180
|
+
|
|
1181
|
+
for (const [sourceId, cutoff] of initial) {
|
|
1182
|
+
const observed = current.get(sourceId);
|
|
1183
|
+
const scanBytes = Number.isSafeInteger(Number(cutoff.scanBytes)) &&
|
|
1184
|
+
Number(cutoff.scanBytes) >= 0
|
|
1185
|
+
? Number(cutoff.scanBytes)
|
|
1186
|
+
: cutoff.size;
|
|
1187
|
+
if (!observed || observed.size < scanBytes) return false;
|
|
1188
|
+
const unchanged =
|
|
1189
|
+
observed.path === cutoff.path &&
|
|
1190
|
+
observed.size === cutoff.size &&
|
|
1191
|
+
observed.mtimeMs === cutoff.mtimeMs &&
|
|
1192
|
+
observed.ctimeMs === cutoff.ctimeMs &&
|
|
1193
|
+
observed.dev === cutoff.dev &&
|
|
1194
|
+
observed.ino === cutoff.ino;
|
|
1195
|
+
if (unchanged) continue;
|
|
1196
|
+
const cutoffFingerprint = primitiveString(cutoff.cursorFingerprint);
|
|
1197
|
+
if (
|
|
1198
|
+
cutoffFingerprint === null ||
|
|
1199
|
+
cutoffFingerprint.length !== 64 ||
|
|
1200
|
+
await prefixFingerprint(observed.path, scanBytes) !==
|
|
1201
|
+
cutoffFingerprint
|
|
1202
|
+
) {
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return true;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function acceptedSourceWatermark(before, after) {
|
|
1210
|
+
const initialRolloutPaths = new Set(
|
|
1211
|
+
before.lifecycleFiles.map((entry) => entry.path),
|
|
1212
|
+
);
|
|
1213
|
+
const rolloutPaths = new Set([
|
|
1214
|
+
...initialRolloutPaths,
|
|
1215
|
+
...after.lifecycleFiles.map((entry) => entry.path),
|
|
1216
|
+
]);
|
|
1217
|
+
const entries = [
|
|
1218
|
+
...before.watermarkEntries.filter(({ path }) =>
|
|
1219
|
+
initialRolloutPaths.has(path)),
|
|
1220
|
+
...after.watermarkEntries.filter(({ path }) => !rolloutPaths.has(path)),
|
|
1221
|
+
]
|
|
1222
|
+
.sort((left, right) => left.path.localeCompare(right.path))
|
|
1223
|
+
.map(({ manifest }) => manifest);
|
|
1224
|
+
return sourceWatermarkFromEntries(entries);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
export async function sourceInventory(codexHome, includeArchived = true) {
|
|
1228
|
+
const root = resolve(codexHome);
|
|
1229
|
+
const activeRolloutFiles = await listJsonlFiles(
|
|
1230
|
+
resolve(codexHome, "sessions"),
|
|
1231
|
+
);
|
|
1232
|
+
// Active-only reports must not parse archived rollouts, but they still need
|
|
1233
|
+
// archive identities to distinguish a move from an unexplained deletion.
|
|
1234
|
+
// Tracking stat-only lifecycle entries prevents the first --no-archived
|
|
1235
|
+
// refresh after a move from continuing to attribute the source as active.
|
|
1236
|
+
const archivedRolloutFiles = await listJsonlFiles(
|
|
1237
|
+
resolve(codexHome, "archived_sessions"),
|
|
1238
|
+
);
|
|
1239
|
+
const lifecycleRolloutFiles = [
|
|
1240
|
+
...activeRolloutFiles,
|
|
1241
|
+
...archivedRolloutFiles,
|
|
1242
|
+
];
|
|
1243
|
+
const rolloutFiles = includeArchived
|
|
1244
|
+
? lifecycleRolloutFiles
|
|
1245
|
+
: activeRolloutFiles;
|
|
1246
|
+
const sqliteFiles = [
|
|
616
1247
|
resolve(codexHome, "state_5.sqlite"),
|
|
617
1248
|
resolve(codexHome, "sqlite", "state_5.sqlite"),
|
|
618
1249
|
];
|
|
1250
|
+
const metadataFiles = [
|
|
1251
|
+
resolve(codexHome, "session_index.jsonl"),
|
|
1252
|
+
...sqliteFiles.flatMap((path) => [path, `${path}-wal`]),
|
|
1253
|
+
];
|
|
619
1254
|
const existingMetadataFiles = [];
|
|
620
1255
|
for (const path of metadataFiles) {
|
|
621
1256
|
if (await pathExists(path)) existingMetadataFiles.push(path);
|
|
622
1257
|
}
|
|
623
|
-
const sourceFiles = [
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1258
|
+
const sourceFiles = [
|
|
1259
|
+
...new Set([...lifecycleRolloutFiles, ...existingMetadataFiles]),
|
|
1260
|
+
]
|
|
1261
|
+
.sort();
|
|
1262
|
+
const sourceStats = await Promise.all(
|
|
1263
|
+
sourceFiles.map(async (path) => ({
|
|
1264
|
+
path,
|
|
1265
|
+
sourceStat: await stat(path),
|
|
1266
|
+
})),
|
|
1267
|
+
);
|
|
1268
|
+
const sourceStatByPath = new Map(
|
|
1269
|
+
sourceStats.map((entry) => [entry.path, entry.sourceStat]),
|
|
1270
|
+
);
|
|
1271
|
+
const entries = sourceStats.map(({ path, sourceStat }) =>
|
|
1272
|
+
sourceManifestEntry(root, path, sourceStat),
|
|
1273
|
+
);
|
|
1274
|
+
const rolloutSourceIds = sourceIdsForRolloutPaths(
|
|
1275
|
+
root,
|
|
1276
|
+
lifecycleRolloutFiles.map((path) => ({
|
|
1277
|
+
path,
|
|
1278
|
+
sourceStat: sourceStatByPath.get(path),
|
|
1279
|
+
})),
|
|
1280
|
+
);
|
|
1281
|
+
const entryForPath = (path) => ({
|
|
1282
|
+
path,
|
|
1283
|
+
size: sourceStatByPath.get(path).size,
|
|
1284
|
+
mtimeMs: sourceStatByPath.get(path).mtimeMs,
|
|
1285
|
+
ctimeMs: sourceStatByPath.get(path).ctimeMs,
|
|
1286
|
+
dev: sourceStatByPath.get(path).dev ?? null,
|
|
1287
|
+
ino: sourceStatByPath.get(path).ino ?? null,
|
|
1288
|
+
sourceId: rolloutSourceIds.get(path),
|
|
1289
|
+
location: sourceLocationForPath(root, path),
|
|
1290
|
+
});
|
|
1291
|
+
const lifecycleFiles = lifecycleRolloutFiles.sort().map(entryForPath);
|
|
1292
|
+
const lifecycleByPath = new Map(
|
|
1293
|
+
lifecycleFiles.map((entry) => [entry.path, entry]),
|
|
1294
|
+
);
|
|
1295
|
+
const lifecyclePathSet = new Set(lifecycleRolloutFiles);
|
|
1296
|
+
const watermarkEntries = sourceStats.map(({ path, sourceStat }) => ({
|
|
1297
|
+
path,
|
|
1298
|
+
manifest: sourceManifestEntry(root, path, sourceStat),
|
|
1299
|
+
}));
|
|
1300
|
+
return {
|
|
1301
|
+
files: rolloutFiles.sort().map((path) => lifecycleByPath.get(path)),
|
|
1302
|
+
lifecycleFiles,
|
|
1303
|
+
watermarkEntries,
|
|
1304
|
+
metadataWatermark: sourceWatermarkFromEntries(
|
|
1305
|
+
watermarkEntries
|
|
1306
|
+
.filter(({ path }) => !lifecyclePathSet.has(path))
|
|
1307
|
+
.map(({ manifest }) => manifest),
|
|
1308
|
+
),
|
|
1309
|
+
watermark: sourceWatermarkFromEntries(entries),
|
|
1310
|
+
cutoffAt: new Date().toISOString(),
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
export async function latestSourceModifiedAt(codexHome, includeArchived = true) {
|
|
1315
|
+
return (
|
|
1316
|
+
await sourceInventory(codexHome, includeArchived)
|
|
1317
|
+
).watermark.latestModifiedAt;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function metadataTimestampMs(value) {
|
|
1321
|
+
if (value == null) return null;
|
|
1322
|
+
if (String(value).trim().length === 0) return null;
|
|
1323
|
+
const numeric = Number(value);
|
|
1324
|
+
if (Number.isFinite(numeric)) {
|
|
1325
|
+
return numeric < 10_000_000_000 ? numeric * 1_000 : numeric;
|
|
1326
|
+
}
|
|
1327
|
+
const parsed = Date.parse(String(value));
|
|
1328
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
627
1329
|
}
|
|
628
1330
|
|
|
629
1331
|
async function readSessionTitles(path) {
|
|
@@ -636,11 +1338,16 @@ async function readSessionTitles(path) {
|
|
|
636
1338
|
try {
|
|
637
1339
|
const record = JSON.parse(line);
|
|
638
1340
|
if (!record?.id || !record?.thread_name) continue;
|
|
639
|
-
const timestamp =
|
|
1341
|
+
const timestamp = metadataTimestampMs(record.updated_at);
|
|
640
1342
|
const current = titles.get(record.id);
|
|
641
|
-
if (
|
|
1343
|
+
if (
|
|
1344
|
+
!current ||
|
|
1345
|
+
timestamp === null ||
|
|
1346
|
+
current.timestamp === null ||
|
|
1347
|
+
timestamp >= current.timestamp
|
|
1348
|
+
) {
|
|
642
1349
|
titles.set(record.id, {
|
|
643
|
-
title:
|
|
1350
|
+
title: spoolText(record.thread_name).replace(/\s+/g, " "),
|
|
644
1351
|
timestamp,
|
|
645
1352
|
});
|
|
646
1353
|
}
|
|
@@ -651,48 +1358,269 @@ async function readSessionTitles(path) {
|
|
|
651
1358
|
return titles;
|
|
652
1359
|
}
|
|
653
1360
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
1361
|
+
const STATE_DATABASE_BUSY_TIMEOUT_MS = 250;
|
|
1362
|
+
const STATE_THREAD_COLUMNS = [
|
|
1363
|
+
"id",
|
|
1364
|
+
"created_at",
|
|
1365
|
+
"updated_at",
|
|
1366
|
+
"source",
|
|
1367
|
+
"cwd",
|
|
1368
|
+
"title",
|
|
1369
|
+
"name",
|
|
1370
|
+
"tokens_used",
|
|
1371
|
+
"git_sha",
|
|
1372
|
+
"git_branch",
|
|
1373
|
+
"git_origin_url",
|
|
1374
|
+
"agent_nickname",
|
|
1375
|
+
"agent_role",
|
|
1376
|
+
"model",
|
|
1377
|
+
"reasoning_effort",
|
|
1378
|
+
"thread_source",
|
|
1379
|
+
];
|
|
1380
|
+
const ROLLOUT_METADATA_COLUMNS = [
|
|
1381
|
+
"source",
|
|
1382
|
+
"cwd",
|
|
1383
|
+
"git_origin_url",
|
|
1384
|
+
"model",
|
|
1385
|
+
"reasoning_effort",
|
|
1386
|
+
"thread_source",
|
|
1387
|
+
];
|
|
665
1388
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
)
|
|
676
|
-
|
|
677
|
-
|
|
1389
|
+
function currentThreadMetadataFingerprints(state) {
|
|
1390
|
+
const threadIds = new Set(state.rows.keys());
|
|
1391
|
+
const fingerprints = new Map();
|
|
1392
|
+
for (const threadId of threadIds) {
|
|
1393
|
+
const row = state.rows.get(threadId);
|
|
1394
|
+
fingerprints.set(
|
|
1395
|
+
String(threadId),
|
|
1396
|
+
hash(JSON.stringify(
|
|
1397
|
+
ROLLOUT_METADATA_COLUMNS.map((column) => row?.[column] ?? null),
|
|
1398
|
+
), 64),
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
return fingerprints;
|
|
1402
|
+
}
|
|
678
1403
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
1404
|
+
function threadMetadataRecordChanged(current, previous, fields) {
|
|
1405
|
+
if (!previous) return true;
|
|
1406
|
+
return fields.some((field) =>
|
|
1407
|
+
(current?.[field] ?? null) !== (previous?.[field] ?? null)
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function providedThreadMetadataFields(threadId, state) {
|
|
1412
|
+
const row = state.rows.get(threadId);
|
|
1413
|
+
const fields = [];
|
|
1414
|
+
if (row?.cwd != null || row?.git_origin_url != null) fields.push("project");
|
|
1415
|
+
if (row?.model != null) fields.push("model");
|
|
1416
|
+
if (row?.reasoning_effort != null) fields.push("effort");
|
|
1417
|
+
if (row?.thread_source != null || row?.source != null) {
|
|
1418
|
+
fields.push("source", "useType");
|
|
689
1419
|
}
|
|
690
|
-
return
|
|
1420
|
+
return fields;
|
|
691
1421
|
}
|
|
692
1422
|
|
|
693
|
-
function
|
|
694
|
-
|
|
695
|
-
|
|
1423
|
+
function metadataChangedThreadIds({
|
|
1424
|
+
state,
|
|
1425
|
+
titles,
|
|
1426
|
+
currentFingerprints,
|
|
1427
|
+
previousFingerprints,
|
|
1428
|
+
persistedThreads,
|
|
1429
|
+
}) {
|
|
1430
|
+
if (previousFingerprints instanceof Map) {
|
|
1431
|
+
const threadIds = new Set([
|
|
1432
|
+
...currentFingerprints.keys(),
|
|
1433
|
+
...previousFingerprints.keys(),
|
|
1434
|
+
]);
|
|
1435
|
+
return new Set([...threadIds].filter((threadId) =>
|
|
1436
|
+
currentFingerprints.get(threadId) !== previousFingerprints.get(threadId)
|
|
1437
|
+
));
|
|
1438
|
+
}
|
|
1439
|
+
if (!(persistedThreads instanceof Map)) return null;
|
|
1440
|
+
|
|
1441
|
+
// Existing v3 ledgers predate per-thread fingerprints. Seed them without a
|
|
1442
|
+
// one-time full rescan by comparing today's normalized metadata with the
|
|
1443
|
+
// private thread records already stored in the ledger. Ambiguous rollout
|
|
1444
|
+
// names still use the conservative global fallback below.
|
|
1445
|
+
const changed = new Set();
|
|
1446
|
+
for (const threadId of currentFingerprints.keys()) {
|
|
1447
|
+
const current = threadMetadata(
|
|
1448
|
+
threadId,
|
|
1449
|
+
state.rows,
|
|
1450
|
+
titles,
|
|
1451
|
+
state.parents,
|
|
1452
|
+
{},
|
|
1453
|
+
persistedThreads,
|
|
1454
|
+
);
|
|
1455
|
+
const providedFields = providedThreadMetadataFields(threadId, state);
|
|
1456
|
+
if (
|
|
1457
|
+
threadMetadataRecordChanged(
|
|
1458
|
+
current,
|
|
1459
|
+
persistedThreads.get(threadId),
|
|
1460
|
+
providedFields,
|
|
1461
|
+
)
|
|
1462
|
+
) {
|
|
1463
|
+
changed.add(threadId);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
return changed;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function stateDatabaseResult(status, rows, parents, reason = null) {
|
|
1470
|
+
return {
|
|
1471
|
+
rows,
|
|
1472
|
+
parents,
|
|
1473
|
+
metadata: {
|
|
1474
|
+
status,
|
|
1475
|
+
reason,
|
|
1476
|
+
threadRows: rows.size,
|
|
1477
|
+
parentEdges: parents.size,
|
|
1478
|
+
},
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
function stateDatabaseFailureReason(error) {
|
|
1483
|
+
const errorNumber = Number(error?.errcode);
|
|
1484
|
+
if (errorNumber === 5 || errorNumber === 6) return "busy";
|
|
1485
|
+
if (errorNumber === 11 || errorNumber === 26) return "corrupt";
|
|
1486
|
+
const message = String(error?.message || "").toLowerCase();
|
|
1487
|
+
if (message.includes("locked") || message.includes("busy")) return "busy";
|
|
1488
|
+
if (
|
|
1489
|
+
message.includes("corrupt") ||
|
|
1490
|
+
message.includes("malformed") ||
|
|
1491
|
+
message.includes("not a database")
|
|
1492
|
+
) {
|
|
1493
|
+
return "corrupt";
|
|
1494
|
+
}
|
|
1495
|
+
if (message.includes("no such table") || message.includes("no such column")) {
|
|
1496
|
+
return "schema-mismatch";
|
|
1497
|
+
}
|
|
1498
|
+
return "read-error";
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function expectedStateDatabaseFailure(error) {
|
|
1502
|
+
const code = String(error?.code || "");
|
|
1503
|
+
return code.startsWith("ERR_SQLITE") ||
|
|
1504
|
+
["EACCES", "EISDIR", "ENOENT", "ENOTDIR", "EPERM"].includes(code);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function stateTableExists(database, table) {
|
|
1508
|
+
return Boolean(
|
|
1509
|
+
database
|
|
1510
|
+
.prepare(
|
|
1511
|
+
"SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
1512
|
+
)
|
|
1513
|
+
.get(table),
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function stateTableColumns(database, table) {
|
|
1518
|
+
return new Set(
|
|
1519
|
+
database
|
|
1520
|
+
.prepare(`PRAGMA table_info("${table}")`)
|
|
1521
|
+
.all()
|
|
1522
|
+
.map((column) => String(column.name)),
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
async function readState(codexHome) {
|
|
1527
|
+
const preferred = resolve(codexHome, "state_5.sqlite");
|
|
1528
|
+
const legacy = resolve(codexHome, "sqlite", "state_5.sqlite");
|
|
1529
|
+
const path = (await pathExists(preferred))
|
|
1530
|
+
? preferred
|
|
1531
|
+
: (await pathExists(legacy))
|
|
1532
|
+
? legacy
|
|
1533
|
+
: null;
|
|
1534
|
+
const rows = new Map();
|
|
1535
|
+
const parents = new Map();
|
|
1536
|
+
if (!path) return stateDatabaseResult("missing", rows, parents);
|
|
1537
|
+
|
|
1538
|
+
let database;
|
|
1539
|
+
try {
|
|
1540
|
+
database = new DatabaseSync(path, { readOnly: true });
|
|
1541
|
+
database.exec(`PRAGMA busy_timeout = ${STATE_DATABASE_BUSY_TIMEOUT_MS}`);
|
|
1542
|
+
if (!stateTableExists(database, "threads")) {
|
|
1543
|
+
return stateDatabaseResult(
|
|
1544
|
+
"unavailable",
|
|
1545
|
+
rows,
|
|
1546
|
+
parents,
|
|
1547
|
+
"schema-mismatch",
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
const threadColumns = stateTableColumns(database, "threads");
|
|
1551
|
+
if (!threadColumns.has("id")) {
|
|
1552
|
+
return stateDatabaseResult(
|
|
1553
|
+
"unavailable",
|
|
1554
|
+
rows,
|
|
1555
|
+
parents,
|
|
1556
|
+
"schema-mismatch",
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
const projection = STATE_THREAD_COLUMNS.map((column) =>
|
|
1560
|
+
threadColumns.has(column)
|
|
1561
|
+
? `"${column}"`
|
|
1562
|
+
: `NULL AS "${column}"`,
|
|
1563
|
+
).join(", ");
|
|
1564
|
+
const threadRows = database
|
|
1565
|
+
.prepare(`SELECT ${projection} FROM threads`)
|
|
1566
|
+
.all();
|
|
1567
|
+
for (const row of threadRows) rows.set(String(row.id), row);
|
|
1568
|
+
|
|
1569
|
+
if (!stateTableExists(database, "thread_spawn_edges")) {
|
|
1570
|
+
return stateDatabaseResult("available", rows, parents);
|
|
1571
|
+
}
|
|
1572
|
+
const edgeColumns = stateTableColumns(database, "thread_spawn_edges");
|
|
1573
|
+
if (
|
|
1574
|
+
!edgeColumns.has("parent_thread_id") ||
|
|
1575
|
+
!edgeColumns.has("child_thread_id")
|
|
1576
|
+
) {
|
|
1577
|
+
return stateDatabaseResult(
|
|
1578
|
+
"partial",
|
|
1579
|
+
rows,
|
|
1580
|
+
parents,
|
|
1581
|
+
"schema-mismatch",
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
try {
|
|
1585
|
+
const edgeRows = database
|
|
1586
|
+
.prepare(
|
|
1587
|
+
"SELECT parent_thread_id, child_thread_id FROM thread_spawn_edges",
|
|
1588
|
+
)
|
|
1589
|
+
.all();
|
|
1590
|
+
for (const edge of edgeRows) {
|
|
1591
|
+
parents.set(String(edge.child_thread_id), String(edge.parent_thread_id));
|
|
1592
|
+
}
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
if (!expectedStateDatabaseFailure(error)) throw error;
|
|
1595
|
+
return stateDatabaseResult(
|
|
1596
|
+
"partial",
|
|
1597
|
+
rows,
|
|
1598
|
+
parents,
|
|
1599
|
+
stateDatabaseFailureReason(error),
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
return stateDatabaseResult("available", rows, parents);
|
|
1603
|
+
} catch (error) {
|
|
1604
|
+
if (!expectedStateDatabaseFailure(error)) throw error;
|
|
1605
|
+
return stateDatabaseResult(
|
|
1606
|
+
"unavailable",
|
|
1607
|
+
new Map(),
|
|
1608
|
+
new Map(),
|
|
1609
|
+
stateDatabaseFailureReason(error),
|
|
1610
|
+
);
|
|
1611
|
+
} finally {
|
|
1612
|
+
try {
|
|
1613
|
+
database?.close();
|
|
1614
|
+
} catch {
|
|
1615
|
+
// State metadata is optional enrichment; close failures cannot erase
|
|
1616
|
+
// additive usage recovered from rollout JSONL.
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function taskStartCandidate(record, threadId, stateRow, fileContext) {
|
|
1622
|
+
const outerMs = new Date(record.timestamp).getTime();
|
|
1623
|
+
const started = asFiniteNumber(record.payload?.started_at);
|
|
696
1624
|
const startedMs = started > 10_000_000_000 ? started : started * 1_000;
|
|
697
1625
|
const timestamp = isoFromEpoch(started, safeIso(record.timestamp, new Date(0).toISOString()));
|
|
698
1626
|
const deltaMs =
|
|
@@ -714,78 +1642,144 @@ function taskStartCandidate(record, threadId, stateRow, fileContext) {
|
|
|
714
1642
|
};
|
|
715
1643
|
}
|
|
716
1644
|
|
|
717
|
-
function
|
|
718
|
-
|
|
719
|
-
|
|
1645
|
+
function quotaCandidates(rateLimits, occurrence) {
|
|
1646
|
+
if (rateLimits == null) return { candidates: [], invalidRecords: 0 };
|
|
1647
|
+
if (Object(rateLimits) !== rateLimits || Array.isArray(rateLimits)) {
|
|
1648
|
+
return { candidates: [], invalidRecords: 1 };
|
|
1649
|
+
}
|
|
720
1650
|
const {
|
|
721
1651
|
primary,
|
|
722
1652
|
secondary,
|
|
723
1653
|
limit_id: limitId,
|
|
724
1654
|
limit_name: limitName,
|
|
725
1655
|
plan_type: planType,
|
|
726
|
-
} = rateLimits
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1656
|
+
} = rateLimits;
|
|
1657
|
+
const limitIdText = limitId == null ? null : primitiveString(limitId);
|
|
1658
|
+
const limitNameText = limitName == null ? null : primitiveString(limitName);
|
|
1659
|
+
const planTypeText = planType == null ? null : primitiveString(planType);
|
|
1660
|
+
if (
|
|
1661
|
+
(limitId != null && limitIdText == null) ||
|
|
1662
|
+
(limitName != null && limitNameText == null) ||
|
|
1663
|
+
(planType != null && planTypeText == null)
|
|
1664
|
+
) {
|
|
1665
|
+
return { candidates: [], invalidRecords: 1 };
|
|
1666
|
+
}
|
|
1667
|
+
// Codex treats an omitted limit id as the default `codex` bucket. Keep the
|
|
1668
|
+
// provider id authoritative: a human label is optional display metadata and
|
|
1669
|
+
// must never create a second durable identity for the same quota pool.
|
|
1670
|
+
const canonicalLimitId = (limitIdText ?? "")
|
|
1671
|
+
.trim()
|
|
1672
|
+
.replace(/[A-Z-]/g, (character) =>
|
|
1673
|
+
character === "-" ? "_" : character.toLowerCase()) || "codex";
|
|
1674
|
+
const normalizedLimitName = limitNameText == null
|
|
1675
|
+
? null
|
|
1676
|
+
: safeExportLabel(limitNameText, 80, null);
|
|
1677
|
+
const normalizedPlanType = planTypeText == null
|
|
1678
|
+
? "unknown"
|
|
1679
|
+
: safeExportLabel(planTypeText, 80, null);
|
|
1680
|
+
if (normalizedPlanType == null) {
|
|
1681
|
+
return { candidates: [], invalidRecords: 1 };
|
|
1682
|
+
}
|
|
1683
|
+
const scope = canonicalLimitId === "codex" ? "account" : "named";
|
|
1684
|
+
const candidates = [];
|
|
1685
|
+
let invalidRecords = 0;
|
|
1686
|
+
for (const bucket of [primary, secondary]) {
|
|
1687
|
+
// Providers use null for an unavailable secondary window.
|
|
1688
|
+
if (bucket == null) continue;
|
|
1689
|
+
if (Object(bucket) !== bucket || Array.isArray(bucket)) {
|
|
1690
|
+
invalidRecords += 1;
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
const normalized = normalizeQuotaObservationFields({
|
|
1694
|
+
windowMinutes: bucket.window_minutes,
|
|
1695
|
+
usedPercent: bucket.used_percent,
|
|
1696
|
+
resetsAt: bucket.resets_at,
|
|
1697
|
+
});
|
|
1698
|
+
if (!normalized) {
|
|
1699
|
+
invalidRecords += 1;
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
const { windowMinutes, usedPercent, resetsAt } = normalized;
|
|
1703
|
+
const stableLimitKey = hash(canonicalLimitId, 16);
|
|
1704
|
+
const observationKey = durableQuotaObservationKey({
|
|
1705
|
+
limitKey: stableLimitKey,
|
|
736
1706
|
windowMinutes,
|
|
737
1707
|
resetsAt,
|
|
738
1708
|
usedPercent,
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
// storing every repeated provider sample in the snapshot.
|
|
1709
|
+
});
|
|
1710
|
+
candidates.push({
|
|
1711
|
+
observationKey,
|
|
1712
|
+
id: `quota-${hash(observationKey)}`,
|
|
744
1713
|
timestamp: occurrence.timestamp,
|
|
745
1714
|
lastSeenAt: occurrence.timestamp,
|
|
746
1715
|
usedPercent,
|
|
747
1716
|
windowMinutes,
|
|
748
1717
|
resetsAt,
|
|
749
|
-
planType:
|
|
750
|
-
limitKey:
|
|
751
|
-
limitName:
|
|
1718
|
+
planType: normalizedPlanType,
|
|
1719
|
+
limitKey: stableLimitKey,
|
|
1720
|
+
limitName: normalizedLimitName,
|
|
1721
|
+
scope,
|
|
752
1722
|
source: "log",
|
|
753
1723
|
turnId: occurrence.turnId || null,
|
|
754
1724
|
originalLikely: occurrence.originalLikely,
|
|
755
|
-
};
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
return { candidates, invalidRecords };
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function rememberQuotaLabel(quotaLabels, sourceId, candidate) {
|
|
1731
|
+
if (candidate.limitName == null) return;
|
|
1732
|
+
const observedAt = Date.parse(candidate.timestamp);
|
|
1733
|
+
const key = JSON.stringify([String(sourceId), candidate.limitKey]);
|
|
1734
|
+
const current = quotaLabels.get(key);
|
|
1735
|
+
if (
|
|
1736
|
+
!current ||
|
|
1737
|
+
observedAt > current.observedAtMs ||
|
|
1738
|
+
(
|
|
1739
|
+
observedAt === current.observedAtMs &&
|
|
1740
|
+
candidate.limitName > current.limitName
|
|
1741
|
+
)
|
|
1742
|
+
) {
|
|
1743
|
+
quotaLabels.set(key, {
|
|
1744
|
+
sourceId: String(sourceId),
|
|
1745
|
+
limitKey: candidate.limitKey,
|
|
1746
|
+
limitName: candidate.limitName,
|
|
1747
|
+
observedAt: candidate.timestamp,
|
|
1748
|
+
observedAtMs: observedAt,
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function rememberQuota(quotaMap, candidate, sinceMs = -Infinity) {
|
|
1754
|
+
if (Date.parse(candidate.timestamp) < sinceMs) return;
|
|
1755
|
+
const { observationKey, ...storedCandidate } = candidate;
|
|
1756
|
+
const current = quotaMap.get(observationKey);
|
|
1757
|
+
if (
|
|
1758
|
+
!current ||
|
|
1759
|
+
(!current.originalLikely && storedCandidate.originalLikely)
|
|
1760
|
+
) {
|
|
1761
|
+
quotaMap.set(observationKey, storedCandidate);
|
|
1762
|
+
} else if (current.originalLikely === storedCandidate.originalLikely) {
|
|
1763
|
+
// Keep the first and last occurrence of an unchanged reading without
|
|
1764
|
+
// storing every repeated provider sample in the snapshot.
|
|
1765
|
+
quotaMap.set(observationKey, {
|
|
1766
|
+
...current,
|
|
1767
|
+
timestamp:
|
|
1768
|
+
storedCandidate.timestamp < current.timestamp
|
|
1769
|
+
? storedCandidate.timestamp
|
|
1770
|
+
: current.timestamp,
|
|
1771
|
+
lastSeenAt:
|
|
1772
|
+
storedCandidate.timestamp > current.lastSeenAt
|
|
1773
|
+
? storedCandidate.timestamp
|
|
1774
|
+
: current.lastSeenAt,
|
|
1775
|
+
});
|
|
775
1776
|
}
|
|
776
1777
|
}
|
|
777
1778
|
|
|
778
1779
|
function responseCall(record) {
|
|
779
1780
|
if (record.type !== "response_item") return null;
|
|
780
1781
|
const payload = record.payload;
|
|
781
|
-
|
|
782
|
-
"function_call",
|
|
783
|
-
"custom_tool_call",
|
|
784
|
-
"tool_search_call",
|
|
785
|
-
"web_search_call",
|
|
786
|
-
"image_generation_call",
|
|
787
|
-
]);
|
|
788
|
-
if (!payload || !allowed.has(payload.type)) return null;
|
|
1782
|
+
if (!payload || !RELEVANT_CALL_TYPES.has(payload.type)) return null;
|
|
789
1783
|
return {
|
|
790
1784
|
type: payload.type,
|
|
791
1785
|
name: String(payload.name || payload.namespace || payload.type).slice(0, 80),
|
|
@@ -793,10 +1787,249 @@ function responseCall(record) {
|
|
|
793
1787
|
};
|
|
794
1788
|
}
|
|
795
1789
|
|
|
796
|
-
|
|
1790
|
+
function inspectJsonLine(line) {
|
|
1791
|
+
let index = 0;
|
|
1792
|
+
let topType = { kind: "absent" };
|
|
1793
|
+
let payloadType = { kind: "absent" };
|
|
1794
|
+
|
|
1795
|
+
function skipWhitespace() {
|
|
1796
|
+
while (
|
|
1797
|
+
line[index] === " " ||
|
|
1798
|
+
line[index] === "\t" ||
|
|
1799
|
+
line[index] === "\n" ||
|
|
1800
|
+
line[index] === "\r"
|
|
1801
|
+
) {
|
|
1802
|
+
index += 1;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
function parseString() {
|
|
1807
|
+
if (line[index] !== '"') return { valid: false };
|
|
1808
|
+
index += 1;
|
|
1809
|
+
const start = index;
|
|
1810
|
+
let simple = true;
|
|
1811
|
+
while (index < line.length) {
|
|
1812
|
+
const code = line.charCodeAt(index);
|
|
1813
|
+
if (code === 34) {
|
|
1814
|
+
const value = simple ? line.slice(start, index) : null;
|
|
1815
|
+
index += 1;
|
|
1816
|
+
return { valid: true, simple, value };
|
|
1817
|
+
}
|
|
1818
|
+
if (code === 92) {
|
|
1819
|
+
simple = false;
|
|
1820
|
+
index += 1;
|
|
1821
|
+
if (index >= line.length) return { valid: false };
|
|
1822
|
+
if (line[index] === "u") {
|
|
1823
|
+
if (!/^[0-9a-f]{4}$/i.test(line.slice(index + 1, index + 5))) {
|
|
1824
|
+
return { valid: false };
|
|
1825
|
+
}
|
|
1826
|
+
index += 5;
|
|
1827
|
+
} else if ('"\\/bfnrt'.includes(line[index])) {
|
|
1828
|
+
index += 1;
|
|
1829
|
+
} else {
|
|
1830
|
+
return { valid: false };
|
|
1831
|
+
}
|
|
1832
|
+
continue;
|
|
1833
|
+
}
|
|
1834
|
+
if (code < 0x20) return { valid: false };
|
|
1835
|
+
index += 1;
|
|
1836
|
+
}
|
|
1837
|
+
return { valid: false };
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
function parseNumber() {
|
|
1841
|
+
const start = index;
|
|
1842
|
+
if (line[index] === "-") index += 1;
|
|
1843
|
+
if (line[index] === "0") {
|
|
1844
|
+
index += 1;
|
|
1845
|
+
} else if (line[index] >= "1" && line[index] <= "9") {
|
|
1846
|
+
while (line[index] >= "0" && line[index] <= "9") index += 1;
|
|
1847
|
+
} else {
|
|
1848
|
+
return false;
|
|
1849
|
+
}
|
|
1850
|
+
if (line[index] === ".") {
|
|
1851
|
+
index += 1;
|
|
1852
|
+
const fractionStart = index;
|
|
1853
|
+
while (line[index] >= "0" && line[index] <= "9") index += 1;
|
|
1854
|
+
if (index === fractionStart) return false;
|
|
1855
|
+
}
|
|
1856
|
+
if (line[index] === "e" || line[index] === "E") {
|
|
1857
|
+
index += 1;
|
|
1858
|
+
if (line[index] === "+" || line[index] === "-") index += 1;
|
|
1859
|
+
const exponentStart = index;
|
|
1860
|
+
while (line[index] >= "0" && line[index] <= "9") index += 1;
|
|
1861
|
+
if (index === exponentStart) return false;
|
|
1862
|
+
}
|
|
1863
|
+
return index > start;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function parseLiteral(value) {
|
|
1867
|
+
if (line.slice(index, index + value.length) !== value) return false;
|
|
1868
|
+
index += value.length;
|
|
1869
|
+
return true;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
function captureValue() {
|
|
1873
|
+
if (line[index] === '"') {
|
|
1874
|
+
const result = parseString();
|
|
1875
|
+
if (!result.valid) return { valid: false };
|
|
1876
|
+
return result.simple
|
|
1877
|
+
? { valid: true, kind: "string", value: result.value }
|
|
1878
|
+
: { valid: true, kind: "unknown" };
|
|
1879
|
+
}
|
|
1880
|
+
return parseValue()
|
|
1881
|
+
? { valid: true, kind: "other" }
|
|
1882
|
+
: { valid: false };
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
function rememberType(current, captured) {
|
|
1886
|
+
if (current.kind === "unknown") return current;
|
|
1887
|
+
return captured.kind === "unknown"
|
|
1888
|
+
? { kind: "unknown" }
|
|
1889
|
+
: captured.kind === "string"
|
|
1890
|
+
? { kind: "string", value: captured.value }
|
|
1891
|
+
: { kind: "other" };
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function parseObject(scope = "generic") {
|
|
1895
|
+
if (line[index] !== "{") return false;
|
|
1896
|
+
index += 1;
|
|
1897
|
+
skipWhitespace();
|
|
1898
|
+
if (line[index] === "}") {
|
|
1899
|
+
index += 1;
|
|
1900
|
+
return true;
|
|
1901
|
+
}
|
|
1902
|
+
while (index < line.length) {
|
|
1903
|
+
const key = parseString();
|
|
1904
|
+
if (!key.valid) return false;
|
|
1905
|
+
skipWhitespace();
|
|
1906
|
+
if (line[index] !== ":") return false;
|
|
1907
|
+
index += 1;
|
|
1908
|
+
skipWhitespace();
|
|
1909
|
+
|
|
1910
|
+
if (scope === "top" && !key.simple) {
|
|
1911
|
+
topType = { kind: "unknown" };
|
|
1912
|
+
payloadType = { kind: "unknown" };
|
|
1913
|
+
}
|
|
1914
|
+
if (scope === "payload" && !key.simple) {
|
|
1915
|
+
payloadType = { kind: "unknown" };
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
if (scope === "top" && key.simple && key.value === "type") {
|
|
1919
|
+
const captured = captureValue();
|
|
1920
|
+
if (!captured.valid) return false;
|
|
1921
|
+
topType = rememberType(topType, captured);
|
|
1922
|
+
} else if (scope === "top" && key.simple && key.value === "payload") {
|
|
1923
|
+
if (line[index] === "{") {
|
|
1924
|
+
if (!parseObject("payload")) return false;
|
|
1925
|
+
if (payloadType.kind === "absent") {
|
|
1926
|
+
payloadType = { kind: "other" };
|
|
1927
|
+
}
|
|
1928
|
+
} else {
|
|
1929
|
+
const captured = captureValue();
|
|
1930
|
+
if (!captured.valid) return false;
|
|
1931
|
+
payloadType = { kind: "other" };
|
|
1932
|
+
}
|
|
1933
|
+
} else if (scope === "payload" && key.simple && key.value === "type") {
|
|
1934
|
+
const captured = captureValue();
|
|
1935
|
+
if (!captured.valid) return false;
|
|
1936
|
+
payloadType = rememberType(payloadType, captured);
|
|
1937
|
+
} else if (!parseValue()) {
|
|
1938
|
+
return false;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
skipWhitespace();
|
|
1942
|
+
if (line[index] === "}") {
|
|
1943
|
+
index += 1;
|
|
1944
|
+
return true;
|
|
1945
|
+
}
|
|
1946
|
+
if (line[index] !== ",") return false;
|
|
1947
|
+
index += 1;
|
|
1948
|
+
skipWhitespace();
|
|
1949
|
+
}
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function parseArray() {
|
|
1954
|
+
if (line[index] !== "[") return false;
|
|
1955
|
+
index += 1;
|
|
1956
|
+
skipWhitespace();
|
|
1957
|
+
if (line[index] === "]") {
|
|
1958
|
+
index += 1;
|
|
1959
|
+
return true;
|
|
1960
|
+
}
|
|
1961
|
+
while (index < line.length) {
|
|
1962
|
+
if (!parseValue()) return false;
|
|
1963
|
+
skipWhitespace();
|
|
1964
|
+
if (line[index] === "]") {
|
|
1965
|
+
index += 1;
|
|
1966
|
+
return true;
|
|
1967
|
+
}
|
|
1968
|
+
if (line[index] !== ",") return false;
|
|
1969
|
+
index += 1;
|
|
1970
|
+
skipWhitespace();
|
|
1971
|
+
}
|
|
1972
|
+
return false;
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function parseValue() {
|
|
1976
|
+
if (line[index] === '"') return parseString().valid;
|
|
1977
|
+
if (line[index] === "{") return parseObject();
|
|
1978
|
+
if (line[index] === "[") return parseArray();
|
|
1979
|
+
if (line[index] === "t") return parseLiteral("true");
|
|
1980
|
+
if (line[index] === "f") return parseLiteral("false");
|
|
1981
|
+
if (line[index] === "n") return parseLiteral("null");
|
|
1982
|
+
return parseNumber();
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
skipWhitespace();
|
|
1986
|
+
try {
|
|
1987
|
+
if (line[index] !== "{" || !parseObject("top")) return null;
|
|
1988
|
+
} catch {
|
|
1989
|
+
// Deeply nested values can exhaust the call stack before JSON.parse
|
|
1990
|
+
// would; treat that as inconclusive and let the real parser decide.
|
|
1991
|
+
return null;
|
|
1992
|
+
}
|
|
1993
|
+
skipWhitespace();
|
|
1994
|
+
if (index !== line.length) return null;
|
|
1995
|
+
return { topType, payloadType };
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
function lineMayAffectUsage(line) {
|
|
1999
|
+
const inspected = inspectJsonLine(line);
|
|
2000
|
+
if (!inspected) return true;
|
|
2001
|
+
if (inspected.topType.kind === "unknown") return true;
|
|
2002
|
+
if (inspected.topType.kind !== "string") return false;
|
|
2003
|
+
if (inspected.topType.value === "session_meta") return true;
|
|
2004
|
+
if (inspected.topType.value === "turn_context") return true;
|
|
2005
|
+
if (inspected.topType.value === "event_msg") {
|
|
2006
|
+
return (
|
|
2007
|
+
inspected.payloadType.kind === "unknown" ||
|
|
2008
|
+
(inspected.payloadType.kind === "string" &&
|
|
2009
|
+
RELEVANT_EVENT_TYPES.has(inspected.payloadType.value))
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
if (inspected.topType.value === "response_item") {
|
|
2013
|
+
return (
|
|
2014
|
+
inspected.payloadType.kind === "unknown" ||
|
|
2015
|
+
(inspected.payloadType.kind === "string" &&
|
|
2016
|
+
RELEVANT_CALL_TYPES.has(inspected.payloadType.value))
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
return false;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
async function scanRollout(
|
|
2023
|
+
path,
|
|
2024
|
+
stateRows,
|
|
2025
|
+
signal,
|
|
2026
|
+
continuityBytes = null,
|
|
2027
|
+
maximumBytes = null,
|
|
2028
|
+
expectedIdentity = null,
|
|
2029
|
+
) {
|
|
797
2030
|
const match = path.match(UUID_AT_END);
|
|
798
2031
|
const threadId = match?.[1] || `file-${hash(path)}`;
|
|
799
|
-
const stateRow =
|
|
2032
|
+
const stateRow = stateRows.get(threadId);
|
|
800
2033
|
const fileContext = {
|
|
801
2034
|
model: stateRow?.model || "unknown",
|
|
802
2035
|
effort: stateRow?.reasoning_effort || "unknown",
|
|
@@ -806,215 +2039,507 @@ async function scanRollout(path, context) {
|
|
|
806
2039
|
serviceTier: null,
|
|
807
2040
|
};
|
|
808
2041
|
const callOrdinals = new Map();
|
|
2042
|
+
const tokenSignatures = new Map();
|
|
2043
|
+
let nextTokenOrdinal = 0;
|
|
809
2044
|
let currentTurnId = "";
|
|
810
2045
|
let currentCandidate = null;
|
|
811
|
-
let currentCandidateSelected = false;
|
|
812
2046
|
let previousCumulative = null;
|
|
2047
|
+
const operations = [];
|
|
2048
|
+
let parseErrors = 0;
|
|
2049
|
+
let correctionIntervals = 0;
|
|
2050
|
+
let invalidTokenRecords = 0;
|
|
2051
|
+
let invalidQuotaRecords = 0;
|
|
813
2052
|
|
|
814
|
-
const
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
2053
|
+
const contentHash = createHash("sha256");
|
|
2054
|
+
const continuityHash = Number.isSafeInteger(continuityBytes) &&
|
|
2055
|
+
continuityBytes >= 0
|
|
2056
|
+
? createHash("sha256")
|
|
2057
|
+
: null;
|
|
2058
|
+
let continuityRemaining = continuityHash ? continuityBytes : 0;
|
|
2059
|
+
const bounded = Number.isSafeInteger(maximumBytes) && maximumBytes >= 0;
|
|
2060
|
+
const sourceOptions = { signal, autoClose: false };
|
|
2061
|
+
if (bounded) {
|
|
2062
|
+
sourceOptions.start = 0;
|
|
2063
|
+
sourceOptions.end = maximumBytes - 1;
|
|
2064
|
+
}
|
|
2065
|
+
const handle = await open(path, "r");
|
|
2066
|
+
let source;
|
|
2067
|
+
let opened;
|
|
2068
|
+
try {
|
|
2069
|
+
opened = await handle.stat();
|
|
2070
|
+
if (bounded && Number(opened.size) < maximumBytes) {
|
|
2071
|
+
const error = new Error(
|
|
2072
|
+
"Rollout became shorter between inventory and collection.",
|
|
2073
|
+
);
|
|
2074
|
+
error.code = "ERR_SOURCE_SIZE_CHANGED";
|
|
2075
|
+
throw error;
|
|
2076
|
+
}
|
|
2077
|
+
if (
|
|
2078
|
+
expectedIdentity?.device != null &&
|
|
2079
|
+
expectedIdentity?.inode != null &&
|
|
2080
|
+
(
|
|
2081
|
+
Number(opened.dev) !== Number(expectedIdentity.device) ||
|
|
2082
|
+
Number(opened.ino) !== Number(expectedIdentity.inode)
|
|
2083
|
+
)
|
|
2084
|
+
) {
|
|
2085
|
+
const error = new Error(
|
|
2086
|
+
"Rollout identity changed between inventory and collection.",
|
|
2087
|
+
);
|
|
2088
|
+
error.code = "ERR_SOURCE_IDENTITY_CHANGED";
|
|
2089
|
+
throw error;
|
|
2090
|
+
}
|
|
2091
|
+
if (bounded && maximumBytes === 0) {
|
|
2092
|
+
source = Readable.from([]);
|
|
2093
|
+
} else {
|
|
2094
|
+
source = handle.createReadStream(sourceOptions);
|
|
824
2095
|
}
|
|
2096
|
+
} catch (error) {
|
|
2097
|
+
await handle.close().catch(() => {});
|
|
2098
|
+
throw error;
|
|
2099
|
+
}
|
|
2100
|
+
const input = new Transform({
|
|
2101
|
+
transform(chunk, encoding, callback) {
|
|
2102
|
+
contentHash.update(chunk);
|
|
2103
|
+
if (continuityHash && continuityRemaining > 0) {
|
|
2104
|
+
const included = chunk.subarray(
|
|
2105
|
+
0,
|
|
2106
|
+
Math.min(chunk.byteLength, continuityRemaining),
|
|
2107
|
+
);
|
|
2108
|
+
continuityHash.update(included);
|
|
2109
|
+
continuityRemaining -= included.byteLength;
|
|
2110
|
+
}
|
|
2111
|
+
callback(null, chunk);
|
|
2112
|
+
},
|
|
2113
|
+
});
|
|
2114
|
+
source.on("error", (error) => input.destroy(error));
|
|
2115
|
+
source.pipe(input);
|
|
2116
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
2117
|
+
try {
|
|
2118
|
+
for await (const line of lines) {
|
|
2119
|
+
if (!line.trim() || !lineMayAffectUsage(line)) continue;
|
|
2120
|
+
let record;
|
|
2121
|
+
try {
|
|
2122
|
+
record = JSON.parse(line);
|
|
2123
|
+
} catch {
|
|
2124
|
+
parseErrors += 1;
|
|
2125
|
+
continue;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
if (record.type === "session_meta") {
|
|
2129
|
+
const payload = record.payload;
|
|
2130
|
+
if (payload?.id === threadId) {
|
|
2131
|
+
fileContext.cwd = payload.cwd || fileContext.cwd;
|
|
2132
|
+
fileContext.gitOrigin =
|
|
2133
|
+
payload.git?.repository_url || fileContext.gitOrigin;
|
|
2134
|
+
fileContext.rawSource = payload.source || fileContext.rawSource;
|
|
2135
|
+
if (payload.parent_thread_id || payload.forked_from_id) {
|
|
2136
|
+
operations.push({
|
|
2137
|
+
kind: "parent",
|
|
2138
|
+
threadId,
|
|
2139
|
+
parentThreadId: String(
|
|
2140
|
+
payload.parent_thread_id || payload.forked_from_id,
|
|
2141
|
+
),
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
const spawnedParent =
|
|
2145
|
+
payload.source?.subagent?.thread_spawn?.parent_thread_id;
|
|
2146
|
+
if (spawnedParent) {
|
|
2147
|
+
operations.push({
|
|
2148
|
+
kind: "parent",
|
|
2149
|
+
threadId,
|
|
2150
|
+
parentThreadId: String(spawnedParent),
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
continue;
|
|
2155
|
+
}
|
|
825
2156
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
payload.git?.repository_url || fileContext.gitOrigin;
|
|
832
|
-
fileContext.rawSource = payload.source || fileContext.rawSource;
|
|
833
|
-
if (payload.parent_thread_id || payload.forked_from_id) {
|
|
834
|
-
context.parents.set(
|
|
2157
|
+
if (record.type === "event_msg" && record.payload?.type === "task_started") {
|
|
2158
|
+
currentTurnId = String(record.payload.turn_id || currentTurnId || "");
|
|
2159
|
+
if (currentTurnId) {
|
|
2160
|
+
currentCandidate = taskStartCandidate(
|
|
2161
|
+
record,
|
|
835
2162
|
threadId,
|
|
836
|
-
|
|
2163
|
+
stateRow,
|
|
2164
|
+
fileContext,
|
|
837
2165
|
);
|
|
2166
|
+
operations.push({
|
|
2167
|
+
kind: "origin",
|
|
2168
|
+
candidate: { ...currentCandidate },
|
|
2169
|
+
});
|
|
838
2170
|
}
|
|
839
|
-
|
|
840
|
-
payload.source?.subagent?.thread_spawn?.parent_thread_id;
|
|
841
|
-
if (spawnedParent) context.parents.set(threadId, String(spawnedParent));
|
|
2171
|
+
continue;
|
|
842
2172
|
}
|
|
843
|
-
continue;
|
|
844
|
-
}
|
|
845
2173
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
fileContext
|
|
854
|
-
|
|
855
|
-
|
|
2174
|
+
if (record.type === "turn_context") {
|
|
2175
|
+
currentTurnId = String(record.payload?.turn_id || currentTurnId || "");
|
|
2176
|
+
fileContext.model = record.payload?.model || fileContext.model;
|
|
2177
|
+
fileContext.effort = record.payload?.effort || fileContext.effort;
|
|
2178
|
+
fileContext.cwd = record.payload?.cwd || fileContext.cwd;
|
|
2179
|
+
if (currentCandidate?.turnId === currentTurnId) {
|
|
2180
|
+
currentCandidate.model = fileContext.model;
|
|
2181
|
+
currentCandidate.effort = fileContext.effort;
|
|
2182
|
+
currentCandidate.cwd = fileContext.cwd;
|
|
2183
|
+
operations.push({
|
|
2184
|
+
kind: "origin_update",
|
|
2185
|
+
turnId: currentTurnId,
|
|
2186
|
+
model: fileContext.model,
|
|
2187
|
+
effort: fileContext.effort,
|
|
2188
|
+
cwd: fileContext.cwd,
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
continue;
|
|
856
2192
|
}
|
|
857
|
-
continue;
|
|
858
|
-
}
|
|
859
2193
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
2194
|
+
if (
|
|
2195
|
+
record.type === "event_msg" &&
|
|
2196
|
+
record.payload?.type === "thread_settings_applied"
|
|
2197
|
+
) {
|
|
2198
|
+
const settings = record.payload?.thread_settings;
|
|
2199
|
+
fileContext.model = settings?.model || fileContext.model;
|
|
2200
|
+
fileContext.effort = settings?.reasoning_effort || fileContext.effort;
|
|
2201
|
+
const serviceTier = String(settings?.service_tier ?? "").trim();
|
|
2202
|
+
fileContext.serviceTier = serviceTier
|
|
2203
|
+
? serviceTier.slice(0, 40)
|
|
2204
|
+
: null;
|
|
2205
|
+
continue;
|
|
872
2206
|
}
|
|
873
|
-
continue;
|
|
874
|
-
}
|
|
875
2207
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
const settings = record.payload?.thread_settings;
|
|
881
|
-
fileContext.model = settings?.model || fileContext.model;
|
|
882
|
-
fileContext.effort = settings?.reasoning_effort || fileContext.effort;
|
|
883
|
-
const serviceTier = String(settings?.service_tier ?? "").trim();
|
|
884
|
-
fileContext.serviceTier = serviceTier
|
|
885
|
-
? serviceTier.slice(0, 40)
|
|
2208
|
+
const tokenCountRecord =
|
|
2209
|
+
record.type === "event_msg" && record.payload?.type === "token_count";
|
|
2210
|
+
const tokenTimestamp = tokenCountRecord
|
|
2211
|
+
? liveRecordTimestamp(record.timestamp)
|
|
886
2212
|
: null;
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
timestamp: safeIso(
|
|
895
|
-
record.timestamp,
|
|
896
|
-
currentCandidate?.timestamp || new Date(0).toISOString(),
|
|
897
|
-
),
|
|
898
|
-
originalLikely,
|
|
899
|
-
model: fileContext.model,
|
|
900
|
-
effort: fileContext.effort,
|
|
901
|
-
cwd: fileContext.cwd,
|
|
902
|
-
gitOrigin: fileContext.gitOrigin,
|
|
903
|
-
rawSource: fileContext.rawSource,
|
|
904
|
-
serviceTier: fileContext.serviceTier,
|
|
905
|
-
};
|
|
2213
|
+
if (tokenCountRecord && tokenTimestamp === null) {
|
|
2214
|
+
if (record.payload?.info?.last_token_usage !== undefined) {
|
|
2215
|
+
invalidTokenRecords += 1;
|
|
2216
|
+
}
|
|
2217
|
+
if (record.payload?.rate_limits != null) invalidQuotaRecords += 1;
|
|
2218
|
+
continue;
|
|
2219
|
+
}
|
|
906
2220
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
const ordinalBase = `${currentTurnId}|${call.type}|${call.name}`;
|
|
910
|
-
const ordinal = (callOrdinals.get(ordinalBase) || 0) + 1;
|
|
911
|
-
callOrdinals.set(ordinalBase, ordinal);
|
|
912
|
-
const callKey = call.stableId
|
|
913
|
-
? `id|${call.stableId}`
|
|
914
|
-
: `ordinal|${ordinalBase}|${ordinal}`;
|
|
915
|
-
context.spool.insertCall(
|
|
916
|
-
callKey,
|
|
917
|
-
currentTurnId,
|
|
2221
|
+
const originalLikely = Boolean(currentCandidate?.deltaMs <= 2_000);
|
|
2222
|
+
const occurrence = {
|
|
918
2223
|
threadId,
|
|
2224
|
+
turnId: currentTurnId,
|
|
2225
|
+
timestamp: tokenTimestamp || safeIso(
|
|
2226
|
+
record.timestamp,
|
|
2227
|
+
currentCandidate?.timestamp || new Date(0).toISOString(),
|
|
2228
|
+
),
|
|
919
2229
|
originalLikely,
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
2230
|
+
model: fileContext.model,
|
|
2231
|
+
effort: fileContext.effort,
|
|
2232
|
+
cwd: fileContext.cwd,
|
|
2233
|
+
gitOrigin: fileContext.gitOrigin,
|
|
2234
|
+
rawSource: fileContext.rawSource,
|
|
2235
|
+
serviceTier: fileContext.serviceTier,
|
|
2236
|
+
};
|
|
923
2237
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
2238
|
+
const call = responseCall(record);
|
|
2239
|
+
if (call) {
|
|
2240
|
+
const ordinalBase = `${currentTurnId}|${call.type}|${call.name}`;
|
|
2241
|
+
const ordinal = (callOrdinals.get(ordinalBase) || 0) + 1;
|
|
2242
|
+
callOrdinals.set(ordinalBase, ordinal);
|
|
2243
|
+
const callKey = call.stableId
|
|
2244
|
+
? `id|${call.stableId}`
|
|
2245
|
+
: `ordinal|${ordinalBase}|${ordinal}`;
|
|
2246
|
+
operations.push({
|
|
2247
|
+
kind: "call",
|
|
2248
|
+
callKey,
|
|
2249
|
+
turnId: currentTurnId,
|
|
2250
|
+
threadId,
|
|
2251
|
+
originalLikely,
|
|
2252
|
+
});
|
|
2253
|
+
continue;
|
|
2254
|
+
}
|
|
927
2255
|
|
|
928
|
-
|
|
929
|
-
const info = record.payload.info;
|
|
930
|
-
if (!info?.last_token_usage) continue;
|
|
2256
|
+
if (!tokenCountRecord) continue;
|
|
931
2257
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
2258
|
+
const info = record.payload.info;
|
|
2259
|
+
const quotaResult = quotaCandidates(
|
|
2260
|
+
record.payload.rate_limits,
|
|
2261
|
+
occurrence,
|
|
2262
|
+
);
|
|
2263
|
+
invalidQuotaRecords += quotaResult.invalidRecords;
|
|
2264
|
+
if (quotaResult.candidates.length > 0) {
|
|
2265
|
+
operations.push({
|
|
2266
|
+
kind: "quota",
|
|
2267
|
+
candidates: quotaResult.candidates,
|
|
2268
|
+
});
|
|
2269
|
+
}
|
|
2270
|
+
if (info?.last_token_usage === undefined) continue;
|
|
944
2271
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
2272
|
+
const totalTuple = tokenTuple(info.total_token_usage);
|
|
2273
|
+
const lastTuple = tokenTuple(info.last_token_usage);
|
|
2274
|
+
if (!lastTuple.valid[5]) {
|
|
2275
|
+
invalidTokenRecords += 1;
|
|
2276
|
+
continue;
|
|
2277
|
+
}
|
|
2278
|
+
if (lastTuple.values[5] <= 0) continue;
|
|
2279
|
+
const contextWindow = asFiniteNumber(info.model_context_window);
|
|
2280
|
+
const tokenSignature = JSON.stringify([
|
|
2281
|
+
totalTuple.values,
|
|
2282
|
+
totalTuple.valid,
|
|
2283
|
+
lastTuple.values,
|
|
2284
|
+
lastTuple.valid,
|
|
2285
|
+
contextWindow,
|
|
2286
|
+
]);
|
|
2287
|
+
const signatureScope = currentTurnId || "legacy";
|
|
2288
|
+
const signatures = tokenSignatures.get(signatureScope) || new Map();
|
|
2289
|
+
let eventOrdinal = signatures.get(tokenSignature);
|
|
2290
|
+
if (!eventOrdinal) {
|
|
2291
|
+
nextTokenOrdinal += 1;
|
|
2292
|
+
const ordinal = nextTokenOrdinal;
|
|
2293
|
+
eventOrdinal = ordinal;
|
|
2294
|
+
signatures.set(tokenSignature, eventOrdinal);
|
|
2295
|
+
tokenSignatures.set(signatureScope, signatures);
|
|
2296
|
+
}
|
|
2297
|
+
const eventKey = currentTurnId
|
|
2298
|
+
? JSON.stringify([
|
|
2299
|
+
currentTurnId,
|
|
2300
|
+
totalTuple.values,
|
|
2301
|
+
totalTuple.valid,
|
|
2302
|
+
lastTuple.values,
|
|
2303
|
+
lastTuple.valid,
|
|
2304
|
+
contextWindow,
|
|
2305
|
+
])
|
|
2306
|
+
: JSON.stringify([
|
|
2307
|
+
"legacy",
|
|
2308
|
+
totalTuple.values,
|
|
2309
|
+
totalTuple.valid,
|
|
2310
|
+
lastTuple.values,
|
|
2311
|
+
lastTuple.valid,
|
|
2312
|
+
contextWindow,
|
|
2313
|
+
]);
|
|
952
2314
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
2315
|
+
if (
|
|
2316
|
+
totalTuple.valid[5] &&
|
|
2317
|
+
previousCumulative !== null &&
|
|
2318
|
+
totalTuple.values[5] < previousCumulative
|
|
2319
|
+
) {
|
|
2320
|
+
correctionIntervals += 1;
|
|
2321
|
+
}
|
|
2322
|
+
previousCumulative = totalTuple.valid[5] ? totalTuple.values[5] : null;
|
|
2323
|
+
|
|
2324
|
+
operations.push({
|
|
2325
|
+
kind: "token",
|
|
2326
|
+
eventKey,
|
|
2327
|
+
turnId: currentTurnId,
|
|
2328
|
+
usage: usageFromTuple(lastTuple),
|
|
2329
|
+
occurrence,
|
|
2330
|
+
eventOrdinal,
|
|
2331
|
+
originalLikely,
|
|
2332
|
+
});
|
|
962
2333
|
}
|
|
2334
|
+
} finally {
|
|
2335
|
+
lines.close();
|
|
963
2336
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
2337
|
+
try {
|
|
2338
|
+
const cursorFingerprint = contentHash.digest("hex");
|
|
2339
|
+
await revalidateScannedRollout(
|
|
2340
|
+
path,
|
|
2341
|
+
handle,
|
|
2342
|
+
opened,
|
|
2343
|
+
expectedIdentity,
|
|
2344
|
+
maximumBytes,
|
|
2345
|
+
cursorFingerprint,
|
|
2346
|
+
);
|
|
2347
|
+
return {
|
|
2348
|
+
path,
|
|
2349
|
+
cursorFingerprint,
|
|
2350
|
+
continuityBytes,
|
|
2351
|
+
continuityFingerprint: continuityHash && continuityRemaining === 0
|
|
2352
|
+
? continuityHash.digest("hex")
|
|
2353
|
+
: null,
|
|
2354
|
+
operations,
|
|
2355
|
+
parseErrors,
|
|
2356
|
+
correctionIntervals,
|
|
2357
|
+
invalidTokenRecords,
|
|
2358
|
+
invalidQuotaRecords,
|
|
2359
|
+
};
|
|
2360
|
+
} finally {
|
|
2361
|
+
await handle.close().catch(() => {});
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
function threadMetadata(
|
|
2366
|
+
threadId,
|
|
2367
|
+
stateRows,
|
|
2368
|
+
titles,
|
|
2369
|
+
parents,
|
|
2370
|
+
fallback = {},
|
|
2371
|
+
persistedRows = new Map(),
|
|
2372
|
+
) {
|
|
2373
|
+
const row = stateRows.get(threadId);
|
|
2374
|
+
const persisted = persistedRows.get(threadId);
|
|
2375
|
+
const metadataRow = row || persisted;
|
|
968
2376
|
const sessionTitle = titles.get(threadId)?.title;
|
|
969
2377
|
const labels = sourceLabels(
|
|
970
|
-
|
|
971
|
-
fallback.rawSource ||
|
|
2378
|
+
fallback.source || metadataRow?.thread_source || metadataRow?.source,
|
|
2379
|
+
fallback.rawSource || metadataRow?.source,
|
|
972
2380
|
);
|
|
973
2381
|
return {
|
|
974
2382
|
id: threadId,
|
|
975
|
-
title:
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
2383
|
+
title: safeExportLabel(
|
|
2384
|
+
fallback.title || sessionTitle || metadataRow?.title || metadataRow?.name,
|
|
2385
|
+
180,
|
|
2386
|
+
safeTitle(metadataRow, sessionTitle),
|
|
2387
|
+
),
|
|
2388
|
+
project: safeExportLabel(
|
|
2389
|
+
fallback.project || metadataRow?.project || projectLabel(
|
|
2390
|
+
fallback.cwd || metadataRow?.cwd,
|
|
2391
|
+
fallback.gitOrigin || metadataRow?.git_origin_url,
|
|
2392
|
+
),
|
|
2393
|
+
160,
|
|
2394
|
+
"Unknown project",
|
|
2395
|
+
),
|
|
2396
|
+
model: safeExportLabel(
|
|
2397
|
+
normalizeCodexCreditModel(
|
|
2398
|
+
fallback.model || metadataRow?.model || "unknown",
|
|
2399
|
+
),
|
|
2400
|
+
80,
|
|
2401
|
+
"unknown",
|
|
2402
|
+
),
|
|
2403
|
+
effort: safeExportLabel(
|
|
2404
|
+
fallback.effort || metadataRow?.reasoning_effort || metadataRow?.effort ||
|
|
2405
|
+
"unknown",
|
|
2406
|
+
40,
|
|
2407
|
+
"unknown",
|
|
979
2408
|
),
|
|
980
|
-
model: normalizeModel(fallback.model || row?.model || "unknown"),
|
|
981
|
-
effort: String(
|
|
982
|
-
fallback.effort || row?.reasoning_effort || "unknown",
|
|
983
|
-
).slice(0, 40),
|
|
984
2409
|
source: labels.source,
|
|
985
2410
|
useType: labels.useType,
|
|
986
|
-
parentThreadId:
|
|
2411
|
+
parentThreadId: safeExportLabel(
|
|
2412
|
+
fallback.parentThreadId ||
|
|
2413
|
+
parents.get(threadId) ||
|
|
2414
|
+
metadataRow?.parentThreadId,
|
|
2415
|
+
80,
|
|
2416
|
+
null,
|
|
2417
|
+
),
|
|
987
2418
|
reportedCumulativeTokens:
|
|
988
|
-
|
|
989
|
-
|
|
2419
|
+
metadataRow && isValidTokenValue(
|
|
2420
|
+
fallback.reportedCumulativeTokens ?? metadataRow.tokens_used ??
|
|
2421
|
+
metadataRow.reportedCumulativeTokens,
|
|
2422
|
+
)
|
|
2423
|
+
? fallback.reportedCumulativeTokens ?? metadataRow.tokens_used ??
|
|
2424
|
+
metadataRow.reportedCumulativeTokens
|
|
990
2425
|
: null,
|
|
991
|
-
createdAt: isoFromEpoch(
|
|
992
|
-
|
|
2426
|
+
createdAt: fallback.createdAt || isoFromEpoch(
|
|
2427
|
+
metadataRow?.created_at || metadataRow?.createdAt,
|
|
2428
|
+
),
|
|
2429
|
+
updatedAt: fallback.updatedAt || isoFromEpoch(
|
|
2430
|
+
metadataRow?.updated_at || metadataRow?.updatedAt,
|
|
2431
|
+
),
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
const TIMESTAMP_CORRECTION_KIND = "before-own-turn";
|
|
2436
|
+
|
|
2437
|
+
function timestampCorrectionOrigin(value, originalTimestamp, fallbackTimestamp) {
|
|
2438
|
+
const parsed = parsedRangeAllocationOrigin(value);
|
|
2439
|
+
const origin = parsed && !Array.isArray(parsed) ? { ...parsed } : {};
|
|
2440
|
+
const current = origin.timestampCorrection;
|
|
2441
|
+
if (
|
|
2442
|
+
current &&
|
|
2443
|
+
!Array.isArray(current) &&
|
|
2444
|
+
Object(current) === current &&
|
|
2445
|
+
current.kind === TIMESTAMP_CORRECTION_KIND &&
|
|
2446
|
+
current.originalTimestamp === originalTimestamp &&
|
|
2447
|
+
current.fallbackTimestamp === fallbackTimestamp
|
|
2448
|
+
) {
|
|
2449
|
+
return origin;
|
|
2450
|
+
}
|
|
2451
|
+
origin.timestampCorrection = {
|
|
2452
|
+
kind: TIMESTAMP_CORRECTION_KIND,
|
|
2453
|
+
originalTimestamp,
|
|
2454
|
+
fallbackTimestamp,
|
|
2455
|
+
};
|
|
2456
|
+
return origin;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
function timestampRepairForToken(token) {
|
|
2460
|
+
if (!token || typeof token !== "object") return null;
|
|
2461
|
+
if (
|
|
2462
|
+
token.identityKind != null &&
|
|
2463
|
+
primitiveString(token.identityKind) !== "exact"
|
|
2464
|
+
) return null;
|
|
2465
|
+
const turnId = primitiveString(token.turnId)?.trim();
|
|
2466
|
+
const threadId = primitiveString(token.threadId)?.trim();
|
|
2467
|
+
const originThreadId = primitiveString(token.originThreadId)?.trim();
|
|
2468
|
+
if (!turnId || !threadId || !originThreadId || threadId !== originThreadId) {
|
|
2469
|
+
return null;
|
|
2470
|
+
}
|
|
2471
|
+
const tokenTimestamp = token.timestamp == null
|
|
2472
|
+
? null
|
|
2473
|
+
: safeIso(token.timestamp, null);
|
|
2474
|
+
const originTimestamp = token.originTimestamp == null
|
|
2475
|
+
? null
|
|
2476
|
+
: safeIso(token.originTimestamp, null);
|
|
2477
|
+
if (!tokenTimestamp || !originTimestamp) return null;
|
|
2478
|
+
const tokenMs = Date.parse(tokenTimestamp);
|
|
2479
|
+
const originMs = Date.parse(originTimestamp);
|
|
2480
|
+
if (!Number.isFinite(tokenMs) || !Number.isFinite(originMs)) return null;
|
|
2481
|
+
// There is no clock-skew tolerance here: an event before its own known turn
|
|
2482
|
+
// is impossible, and the turn origin is the only defensible fallback.
|
|
2483
|
+
if (tokenMs >= originMs) return null;
|
|
2484
|
+
return {
|
|
2485
|
+
timestamp: originTimestamp,
|
|
2486
|
+
rangeAllocationEstimated: true,
|
|
2487
|
+
rangeAllocationOrigin: timestampCorrectionOrigin(
|
|
2488
|
+
token.rangeAllocationOrigin,
|
|
2489
|
+
tokenTimestamp,
|
|
2490
|
+
originTimestamp,
|
|
2491
|
+
),
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
function repairTokenTimestamp(token) {
|
|
2496
|
+
const repair = timestampRepairForToken(token);
|
|
2497
|
+
if (!repair) return token;
|
|
2498
|
+
return {
|
|
2499
|
+
...token,
|
|
2500
|
+
timestamp: repair.timestamp,
|
|
2501
|
+
rangeAllocationEstimated: repair.rangeAllocationEstimated,
|
|
2502
|
+
rangeAllocationOrigin: repair.rangeAllocationOrigin,
|
|
993
2503
|
};
|
|
994
2504
|
}
|
|
995
2505
|
|
|
996
2506
|
function resolvedOccurrence(token) {
|
|
997
|
-
const
|
|
2507
|
+
const resolvedToken = repairTokenTimestamp(token);
|
|
2508
|
+
const origin = resolvedToken.originThreadId == null
|
|
998
2509
|
? null
|
|
999
2510
|
: {
|
|
1000
|
-
threadId:
|
|
1001
|
-
timestamp:
|
|
1002
|
-
model:
|
|
1003
|
-
effort:
|
|
1004
|
-
cwd:
|
|
1005
|
-
gitOrigin:
|
|
1006
|
-
rawSource:
|
|
1007
|
-
serviceTier:
|
|
2511
|
+
threadId: resolvedToken.originThreadId,
|
|
2512
|
+
timestamp: resolvedToken.originTimestamp,
|
|
2513
|
+
model: resolvedToken.originModel,
|
|
2514
|
+
effort: resolvedToken.originEffort,
|
|
2515
|
+
cwd: resolvedToken.originCwd,
|
|
2516
|
+
gitOrigin: resolvedToken.originGitOrigin,
|
|
2517
|
+
rawSource: resolvedToken.originRawSource,
|
|
2518
|
+
serviceTier: resolvedToken.originServiceTier,
|
|
1008
2519
|
};
|
|
1009
|
-
const occurrence =
|
|
2520
|
+
const occurrence = resolvedToken.originalLikely || !origin
|
|
2521
|
+
? resolvedToken
|
|
2522
|
+
: origin;
|
|
1010
2523
|
return {
|
|
1011
2524
|
origin,
|
|
1012
2525
|
occurrence,
|
|
1013
2526
|
threadId: origin?.threadId || occurrence.threadId,
|
|
1014
|
-
timestamp:
|
|
2527
|
+
timestamp: resolvedToken.timestamp || origin?.timestamp,
|
|
1015
2528
|
};
|
|
1016
2529
|
}
|
|
1017
2530
|
|
|
2531
|
+
function parsedRangeAllocationOrigin(value) {
|
|
2532
|
+
if (value && Object(value) === value) return value;
|
|
2533
|
+
const source = primitiveString(value);
|
|
2534
|
+
if (source === null || source.length === 0) return null;
|
|
2535
|
+
try {
|
|
2536
|
+
const parsed = JSON.parse(source);
|
|
2537
|
+
return parsed && Object(parsed) === parsed ? parsed : null;
|
|
2538
|
+
} catch {
|
|
2539
|
+
return null;
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
|
|
1018
2543
|
function newThreadAggregate(metadata, timestamp) {
|
|
1019
2544
|
return {
|
|
1020
2545
|
metadata,
|
|
@@ -1030,33 +2555,131 @@ function newThreadAggregate(metadata, timestamp) {
|
|
|
1030
2555
|
unknownBreakdownTokens: 0,
|
|
1031
2556
|
rateCardCredits: 0,
|
|
1032
2557
|
ratedTokens: 0,
|
|
2558
|
+
hasPositiveUnrated: false,
|
|
1033
2559
|
eventCount: 0,
|
|
1034
2560
|
};
|
|
1035
2561
|
}
|
|
1036
2562
|
|
|
1037
2563
|
function addToThreadAggregate(aggregate, event) {
|
|
2564
|
+
const allowFractional = event.rangeAllocationEstimated === true;
|
|
1038
2565
|
aggregate.lastActiveAt = event.timestamp;
|
|
1039
|
-
aggregate.inputTokens
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
aggregate.
|
|
1045
|
-
|
|
2566
|
+
aggregate.inputTokens = checkedTokenAdd(
|
|
2567
|
+
aggregate.inputTokens,
|
|
2568
|
+
event.inputTokens,
|
|
2569
|
+
{ allowFractional },
|
|
2570
|
+
);
|
|
2571
|
+
aggregate.cachedInputTokens = checkedTokenAdd(
|
|
2572
|
+
aggregate.cachedInputTokens,
|
|
2573
|
+
event.cachedInputTokens,
|
|
2574
|
+
{ allowFractional },
|
|
2575
|
+
);
|
|
2576
|
+
aggregate.outputTokens = checkedTokenAdd(
|
|
2577
|
+
aggregate.outputTokens,
|
|
2578
|
+
event.outputTokens,
|
|
2579
|
+
{ allowFractional },
|
|
2580
|
+
);
|
|
2581
|
+
aggregate.reasoningTokens = checkedTokenAdd(
|
|
2582
|
+
aggregate.reasoningTokens,
|
|
2583
|
+
event.reasoningTokens,
|
|
2584
|
+
{ allowFractional },
|
|
2585
|
+
);
|
|
2586
|
+
aggregate.totalTokens = checkedTokenAdd(
|
|
2587
|
+
aggregate.totalTokens,
|
|
2588
|
+
event.totalTokens,
|
|
2589
|
+
{ allowFractional },
|
|
2590
|
+
);
|
|
2591
|
+
aggregate.toolCalls = checkedTokenAdd(
|
|
2592
|
+
aggregate.toolCalls,
|
|
2593
|
+
event.toolCalls,
|
|
2594
|
+
{ allowFractional },
|
|
2595
|
+
);
|
|
2596
|
+
aggregate.cachedInputTokens = Math.min(
|
|
2597
|
+
aggregate.inputTokens,
|
|
2598
|
+
aggregate.cachedInputTokens,
|
|
2599
|
+
);
|
|
2600
|
+
aggregate.reasoningTokens = Math.min(
|
|
2601
|
+
aggregate.outputTokens,
|
|
2602
|
+
aggregate.reasoningTokens,
|
|
2603
|
+
);
|
|
2604
|
+
aggregate.eventCount = checkedTokenAdd(
|
|
2605
|
+
aggregate.eventCount,
|
|
2606
|
+
Number.isFinite(event.callCount) && event.callCount > 0
|
|
2607
|
+
? event.callCount
|
|
2608
|
+
: 1,
|
|
2609
|
+
{ allowFractional: true },
|
|
2610
|
+
);
|
|
1046
2611
|
if (event.breakdownAvailable) {
|
|
1047
|
-
aggregate.detailedTokens
|
|
2612
|
+
aggregate.detailedTokens = checkedTokenAdd(
|
|
2613
|
+
aggregate.detailedTokens,
|
|
2614
|
+
event.totalTokens,
|
|
2615
|
+
{ allowFractional },
|
|
2616
|
+
);
|
|
1048
2617
|
} else {
|
|
1049
|
-
aggregate.unknownBreakdownTokens
|
|
2618
|
+
aggregate.unknownBreakdownTokens = checkedTokenAdd(
|
|
2619
|
+
aggregate.unknownBreakdownTokens,
|
|
2620
|
+
event.totalTokens,
|
|
2621
|
+
{ allowFractional },
|
|
2622
|
+
);
|
|
1050
2623
|
}
|
|
1051
2624
|
if (event.rateCardCredits !== null) {
|
|
1052
|
-
aggregate.rateCardCredits
|
|
1053
|
-
|
|
2625
|
+
aggregate.rateCardCredits = checkedFiniteAdd(
|
|
2626
|
+
aggregate.rateCardCredits,
|
|
2627
|
+
event.rateCardCredits,
|
|
2628
|
+
);
|
|
2629
|
+
aggregate.ratedTokens = checkedTokenAdd(
|
|
2630
|
+
aggregate.ratedTokens,
|
|
2631
|
+
event.totalTokens,
|
|
2632
|
+
);
|
|
2633
|
+
} else if (event.totalTokens > 0) {
|
|
2634
|
+
aggregate.hasPositiveUnrated = true;
|
|
1054
2635
|
}
|
|
1055
2636
|
}
|
|
1056
2637
|
|
|
2638
|
+
function representableUnknownBreakdownTokens(
|
|
2639
|
+
observedTokens,
|
|
2640
|
+
detailedTokens,
|
|
2641
|
+
unknownBreakdownTokens,
|
|
2642
|
+
) {
|
|
2643
|
+
if (
|
|
2644
|
+
observedTokens >= MAX_SAFE_TOKEN_COUNT &&
|
|
2645
|
+
detailedTokens >= MAX_SAFE_TOKEN_COUNT
|
|
2646
|
+
) {
|
|
2647
|
+
return unknownBreakdownTokens;
|
|
2648
|
+
}
|
|
2649
|
+
return Math.min(
|
|
2650
|
+
unknownBreakdownTokens,
|
|
2651
|
+
Math.max(0, observedTokens - detailedTokens),
|
|
2652
|
+
);
|
|
2653
|
+
}
|
|
2654
|
+
|
|
1057
2655
|
function buildSnapshot(context, options, titles) {
|
|
1058
2656
|
context.spool.finishWrites();
|
|
1059
|
-
const
|
|
2657
|
+
const scope = collectionScope(options);
|
|
2658
|
+
const sinceMs = scope.since === null ? -Infinity : Date.parse(scope.since);
|
|
2659
|
+
|
|
2660
|
+
function scopedTokenTimestamp(token, timestamp) {
|
|
2661
|
+
const timestampMs = Date.parse(timestamp);
|
|
2662
|
+
if (!Number.isFinite(timestampMs)) return null;
|
|
2663
|
+
if (sinceMs === -Infinity || timestampMs >= sinceMs) return timestamp;
|
|
2664
|
+
const range = parsedRangeAllocationOrigin(token.rangeAllocationOrigin);
|
|
2665
|
+
const bounds = [timestampMs, range?.startAt, range?.endAt]
|
|
2666
|
+
.map((value) => Date.parse(String(value ?? "")))
|
|
2667
|
+
.filter(Number.isFinite);
|
|
2668
|
+
const endExclusive = Math.max(...bounds) + 1;
|
|
2669
|
+
if (endExclusive <= sinceMs) return null;
|
|
2670
|
+
const startMs = Math.max(sinceMs, Math.min(...bounds));
|
|
2671
|
+
return new Date(Math.round(
|
|
2672
|
+
startMs + (endExclusive - startMs - 1) / 2,
|
|
2673
|
+
)).toISOString();
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
function scopedEventFragments(event) {
|
|
2677
|
+
if (sinceMs === -Infinity) return [event];
|
|
2678
|
+
return splitUsageBucketsAtBoundaries([event], [sinceMs]).filter(
|
|
2679
|
+
(fragment) => Date.parse(fragment.timestamp) >= sinceMs,
|
|
2680
|
+
);
|
|
2681
|
+
}
|
|
2682
|
+
|
|
1060
2683
|
const lastEventKeyByTurn = new Map();
|
|
1061
2684
|
let earliestEventAt = null;
|
|
1062
2685
|
let latestEventAt = null;
|
|
@@ -1065,9 +2688,20 @@ function buildSnapshot(context, options, titles) {
|
|
|
1065
2688
|
// receives each turn's tool calls without retaining the usage rows.
|
|
1066
2689
|
for (const token of context.spool.tokenRows()) {
|
|
1067
2690
|
const { threadId, timestamp } = resolvedOccurrence(token);
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
2691
|
+
const scopedTimestamp = scopedTokenTimestamp(token, timestamp);
|
|
2692
|
+
if (!scopedTimestamp) continue;
|
|
2693
|
+
if (
|
|
2694
|
+
earliestEventAt === null ||
|
|
2695
|
+
Date.parse(scopedTimestamp) < Date.parse(earliestEventAt)
|
|
2696
|
+
) {
|
|
2697
|
+
earliestEventAt = scopedTimestamp;
|
|
2698
|
+
}
|
|
2699
|
+
if (
|
|
2700
|
+
latestEventAt === null ||
|
|
2701
|
+
Date.parse(scopedTimestamp) > Date.parse(latestEventAt)
|
|
2702
|
+
) {
|
|
2703
|
+
latestEventAt = scopedTimestamp;
|
|
2704
|
+
}
|
|
1071
2705
|
const key = token.turnId || `thread:${threadId}`;
|
|
1072
2706
|
lastEventKeyByTurn.set(key, token.eventKey);
|
|
1073
2707
|
}
|
|
@@ -1079,22 +2713,30 @@ function buildSnapshot(context, options, titles) {
|
|
|
1079
2713
|
}
|
|
1080
2714
|
|
|
1081
2715
|
const threadAggregates = new Map();
|
|
2716
|
+
const coverage = {
|
|
2717
|
+
detailedTokens: 0,
|
|
2718
|
+
unknownBreakdownTokens: 0,
|
|
2719
|
+
};
|
|
1082
2720
|
let observedTokens = 0;
|
|
1083
|
-
let detailedTokens = 0;
|
|
1084
2721
|
let legacyHeuristicEvents = 0;
|
|
1085
2722
|
let observedModelCalls = 0;
|
|
2723
|
+
let exactObservedModelCalls = 0;
|
|
2724
|
+
let migratedCompactedCalls = 0;
|
|
2725
|
+
let migratedCompactedTokens = 0;
|
|
1086
2726
|
|
|
1087
2727
|
function* compactableEvents() {
|
|
1088
2728
|
for (const token of context.spool.tokenRows()) {
|
|
1089
2729
|
const { origin, occurrence, threadId, timestamp } = resolvedOccurrence(token);
|
|
1090
|
-
if (Date.parse(timestamp) < sinceMs) continue;
|
|
1091
2730
|
const metadata = threadMetadata(
|
|
1092
2731
|
threadId,
|
|
1093
2732
|
context.stateRows,
|
|
1094
2733
|
titles,
|
|
1095
2734
|
context.parents,
|
|
1096
2735
|
origin || occurrence,
|
|
2736
|
+
context.persistedThreads,
|
|
1097
2737
|
);
|
|
2738
|
+
const rangeAllocationEstimated =
|
|
2739
|
+
Number(token.rangeAllocationEstimated) === 1;
|
|
1098
2740
|
const usage = {
|
|
1099
2741
|
inputTokens: Number(token.inputTokens),
|
|
1100
2742
|
cachedInputTokens: Number(token.cachedInputTokens),
|
|
@@ -1102,45 +2744,106 @@ function buildSnapshot(context, options, titles) {
|
|
|
1102
2744
|
outputTokens: Number(token.outputTokens),
|
|
1103
2745
|
reasoningTokens: Number(token.reasoningTokens),
|
|
1104
2746
|
totalTokens: Number(token.totalTokens),
|
|
2747
|
+
componentsValid: Number(token.componentsValid) === 1,
|
|
2748
|
+
toolCalls: Number(token.toolCalls),
|
|
2749
|
+
callCount: Number(token.callCount),
|
|
2750
|
+
detailedCallCount: Number(token.detailedCallCount),
|
|
2751
|
+
inputCallCount: Number(token.inputCallCount),
|
|
2752
|
+
rangeAllocationEstimated,
|
|
1105
2753
|
};
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
2754
|
+
const rangeAllocationOrigin = parsedRangeAllocationOrigin(
|
|
2755
|
+
token.rangeAllocationOrigin,
|
|
2756
|
+
);
|
|
2757
|
+
const storedRateCardCredits = token.rateCardCredits == null
|
|
2758
|
+
? null
|
|
2759
|
+
: Number(token.rateCardCredits);
|
|
2760
|
+
const breakdownAvailable = hasDetailedTokenBreakdown(usage);
|
|
2761
|
+
const serviceTier = safeExportLabel(occurrence.serviceTier, 40, null);
|
|
1109
2762
|
const turnKey = token.turnId || `thread:${threadId}`;
|
|
2763
|
+
const sourceToolCalls = lastEventKeyByTurn.get(turnKey) === token.eventKey
|
|
2764
|
+
? toolCounts.get(turnKey) || 0
|
|
2765
|
+
: 0;
|
|
2766
|
+
const persistedToolCalls = Number.isFinite(Number(token.toolCalls))
|
|
2767
|
+
? Math.max(0, Number(token.toolCalls))
|
|
2768
|
+
: 0;
|
|
1110
2769
|
const event = {
|
|
1111
2770
|
...usage,
|
|
1112
2771
|
timestamp,
|
|
2772
|
+
startAt: rangeAllocationOrigin?.startAt || timestamp,
|
|
2773
|
+
endAt: rangeAllocationOrigin?.endAt || timestamp,
|
|
1113
2774
|
threadId,
|
|
1114
|
-
project: metadata.project,
|
|
1115
|
-
model: metadata.model,
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
? toolCounts.get(turnKey) || 0
|
|
1122
|
-
: 0,
|
|
2775
|
+
project: token.project || metadata.project,
|
|
2776
|
+
model: token.displayModel || metadata.model,
|
|
2777
|
+
rateCardModel: token.rateCardModel || occurrence.model || metadata.model,
|
|
2778
|
+
effort: token.effort || metadata.effort,
|
|
2779
|
+
source: token.sourceLabel || metadata.source,
|
|
2780
|
+
useType: token.useType || metadata.useType,
|
|
2781
|
+
toolCalls: Math.max(sourceToolCalls, persistedToolCalls),
|
|
1123
2782
|
serviceTier,
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
2783
|
+
// Keep the canonical model for snapshot display, but preserve the raw
|
|
2784
|
+
// identifier for multiplier selection so unsupported aliases do not
|
|
2785
|
+
// inherit a canonical Daybreak multiplier.
|
|
2786
|
+
rateCardCredits: Number.isFinite(storedRateCardCredits)
|
|
2787
|
+
? storedRateCardCredits
|
|
2788
|
+
: calculateCodexPurchasedCredits({
|
|
2789
|
+
model: occurrence.model || metadata.model,
|
|
2790
|
+
serviceTier,
|
|
2791
|
+
usage,
|
|
2792
|
+
}),
|
|
1130
2793
|
breakdownAvailable,
|
|
2794
|
+
rangeAllocationEstimated,
|
|
2795
|
+
rangeAllocationOrigin,
|
|
1131
2796
|
};
|
|
1132
2797
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
2798
|
+
for (const scopedEvent of scopedEventFragments(event)) {
|
|
2799
|
+
let aggregate = threadAggregates.get(threadId);
|
|
2800
|
+
if (!aggregate) {
|
|
2801
|
+
aggregate = newThreadAggregate(metadata, scopedEvent.timestamp);
|
|
2802
|
+
threadAggregates.set(threadId, aggregate);
|
|
2803
|
+
}
|
|
2804
|
+
addToThreadAggregate(aggregate, scopedEvent);
|
|
2805
|
+
observedTokens = checkedTokenAdd(
|
|
2806
|
+
observedTokens,
|
|
2807
|
+
scopedEvent.totalTokens,
|
|
2808
|
+
{ allowFractional: scopedEvent.rangeAllocationEstimated === true },
|
|
2809
|
+
);
|
|
2810
|
+
checkedTokenPartitionAdd(coverage, scopedEvent.totalTokens, {
|
|
2811
|
+
detailed: scopedEvent.breakdownAvailable,
|
|
2812
|
+
});
|
|
2813
|
+
if (token.identityKind === "exact" && !token.turnId) {
|
|
2814
|
+
legacyHeuristicEvents += 1;
|
|
2815
|
+
}
|
|
2816
|
+
observedModelCalls = checkedTokenAdd(
|
|
2817
|
+
observedModelCalls,
|
|
2818
|
+
Number.isFinite(scopedEvent.callCount) && scopedEvent.callCount > 0
|
|
2819
|
+
? scopedEvent.callCount
|
|
2820
|
+
: 1,
|
|
2821
|
+
{ allowFractional: true },
|
|
2822
|
+
);
|
|
2823
|
+
if (token.identityKind === "migrated_compacted") {
|
|
2824
|
+
migratedCompactedCalls = checkedTokenAdd(
|
|
2825
|
+
migratedCompactedCalls,
|
|
2826
|
+
Number.isFinite(scopedEvent.callCount) && scopedEvent.callCount > 0
|
|
2827
|
+
? scopedEvent.callCount
|
|
2828
|
+
: 1,
|
|
2829
|
+
{ allowFractional: true },
|
|
2830
|
+
);
|
|
2831
|
+
migratedCompactedTokens = checkedTokenAdd(
|
|
2832
|
+
migratedCompactedTokens,
|
|
2833
|
+
scopedEvent.totalTokens,
|
|
2834
|
+
{ allowFractional: scopedEvent.rangeAllocationEstimated === true },
|
|
2835
|
+
);
|
|
2836
|
+
} else {
|
|
2837
|
+
exactObservedModelCalls = checkedTokenAdd(
|
|
2838
|
+
exactObservedModelCalls,
|
|
2839
|
+
Number.isFinite(scopedEvent.callCount) && scopedEvent.callCount > 0
|
|
2840
|
+
? scopedEvent.callCount
|
|
2841
|
+
: 1,
|
|
2842
|
+
{ allowFractional: true },
|
|
2843
|
+
);
|
|
2844
|
+
}
|
|
2845
|
+
yield scopedEvent;
|
|
1137
2846
|
}
|
|
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
2847
|
}
|
|
1145
2848
|
}
|
|
1146
2849
|
|
|
@@ -1151,8 +2854,15 @@ function buildSnapshot(context, options, titles) {
|
|
|
1151
2854
|
toolCounts.clear();
|
|
1152
2855
|
lastEventKeyByTurn.clear();
|
|
1153
2856
|
|
|
1154
|
-
const allThreadIds = new Set(
|
|
2857
|
+
const allThreadIds = new Set(
|
|
2858
|
+
scope.since === null ? context.stateRows.keys() : threadAggregates.keys(),
|
|
2859
|
+
);
|
|
1155
2860
|
for (const threadId of threadAggregates.keys()) allThreadIds.add(threadId);
|
|
2861
|
+
if (scope.since === null) {
|
|
2862
|
+
for (const threadId of context.persistedThreads.keys()) {
|
|
2863
|
+
allThreadIds.add(threadId);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
1156
2866
|
const threads = [];
|
|
1157
2867
|
for (const threadId of allThreadIds) {
|
|
1158
2868
|
const aggregate = threadAggregates.get(threadId);
|
|
@@ -1161,13 +2871,25 @@ function buildSnapshot(context, options, titles) {
|
|
|
1161
2871
|
context.stateRows,
|
|
1162
2872
|
titles,
|
|
1163
2873
|
context.parents,
|
|
2874
|
+
{},
|
|
2875
|
+
context.persistedThreads,
|
|
1164
2876
|
);
|
|
1165
|
-
if (
|
|
2877
|
+
if (
|
|
2878
|
+
!aggregate &&
|
|
2879
|
+
(scope.since !== null || !(metadata.reportedCumulativeTokens > 0))
|
|
2880
|
+
) continue;
|
|
1166
2881
|
const eventCount = aggregate?.eventCount || 0;
|
|
2882
|
+
const unknownBreakdownTokens = aggregate
|
|
2883
|
+
? representableUnknownBreakdownTokens(
|
|
2884
|
+
aggregate.totalTokens,
|
|
2885
|
+
aggregate.detailedTokens,
|
|
2886
|
+
aggregate.unknownBreakdownTokens,
|
|
2887
|
+
)
|
|
2888
|
+
: 0;
|
|
1167
2889
|
const threadCoverage =
|
|
1168
2890
|
eventCount === 0
|
|
1169
2891
|
? "unresolved"
|
|
1170
|
-
: aggregate.
|
|
2892
|
+
: aggregate.unknownBreakdownTokens === 0
|
|
1171
2893
|
? "complete"
|
|
1172
2894
|
: aggregate.detailedTokens > 0
|
|
1173
2895
|
? "partial"
|
|
@@ -1185,7 +2907,7 @@ function buildSnapshot(context, options, titles) {
|
|
|
1185
2907
|
lastActiveAt: aggregate?.lastActiveAt || metadata.updatedAt,
|
|
1186
2908
|
totalTokens: aggregate?.totalTokens || 0,
|
|
1187
2909
|
detailedTokens: aggregate?.detailedTokens || 0,
|
|
1188
|
-
unknownBreakdownTokens
|
|
2910
|
+
unknownBreakdownTokens,
|
|
1189
2911
|
reportedCumulativeTokens: metadata.reportedCumulativeTokens,
|
|
1190
2912
|
inputTokens: aggregate?.inputTokens || 0,
|
|
1191
2913
|
cachedInputTokens: aggregate?.cachedInputTokens || 0,
|
|
@@ -1193,6 +2915,7 @@ function buildSnapshot(context, options, titles) {
|
|
|
1193
2915
|
reasoningTokens: aggregate?.reasoningTokens || 0,
|
|
1194
2916
|
rateCardCredits:
|
|
1195
2917
|
aggregate?.totalTokens > 0 &&
|
|
2918
|
+
!aggregate.hasPositiveUnrated &&
|
|
1196
2919
|
aggregate.ratedTokens === aggregate.totalTokens
|
|
1197
2920
|
? aggregate.rateCardCredits
|
|
1198
2921
|
: null,
|
|
@@ -1213,19 +2936,29 @@ function buildSnapshot(context, options, titles) {
|
|
|
1213
2936
|
const exported = { ...quota };
|
|
1214
2937
|
delete exported.turnId;
|
|
1215
2938
|
delete exported.originalLikely;
|
|
2939
|
+
if (
|
|
2940
|
+
sinceMs !== -Infinity &&
|
|
2941
|
+
Date.parse(exported.timestamp) < sinceMs &&
|
|
2942
|
+
Date.parse(exported.lastSeenAt) >= sinceMs
|
|
2943
|
+
) {
|
|
2944
|
+
exported.timestamp = new Date(sinceMs).toISOString();
|
|
2945
|
+
}
|
|
1216
2946
|
return exported;
|
|
1217
2947
|
})
|
|
1218
2948
|
.filter((quota) => {
|
|
1219
|
-
if (!options.since) return true;
|
|
1220
2949
|
return Date.parse(quota.lastSeenAt) >= sinceMs;
|
|
1221
2950
|
})
|
|
1222
2951
|
.sort(
|
|
1223
2952
|
(left, right) => Date.parse(left.timestamp) - Date.parse(right.timestamp),
|
|
1224
2953
|
);
|
|
1225
2954
|
|
|
1226
|
-
const unknownBreakdownTokens =
|
|
2955
|
+
const { detailedTokens, unknownBreakdownTokens } = coverage;
|
|
2956
|
+
const coverageTokens = detailedTokens + unknownBreakdownTokens;
|
|
1227
2957
|
const stateCounterSumNonAdditive = threads.reduce(
|
|
1228
|
-
(sum, thread) =>
|
|
2958
|
+
(sum, thread) => checkedTokenAdd(
|
|
2959
|
+
sum,
|
|
2960
|
+
thread.reportedCumulativeTokens || 0,
|
|
2961
|
+
),
|
|
1229
2962
|
0,
|
|
1230
2963
|
);
|
|
1231
2964
|
const unresolvedThreadCounters = threads.filter(
|
|
@@ -1237,11 +2970,9 @@ function buildSnapshot(context, options, titles) {
|
|
|
1237
2970
|
(quota) => quota.windowMinutes === WEEK_MINUTES,
|
|
1238
2971
|
);
|
|
1239
2972
|
const accountWideWeekly = weeklyCandidates.filter(
|
|
1240
|
-
(quota) =>
|
|
2973
|
+
(quota) => quota.scope === "account",
|
|
1241
2974
|
);
|
|
1242
|
-
const weekly =
|
|
1243
|
-
...(accountWideWeekly.length ? accountWideWeekly : weeklyCandidates),
|
|
1244
|
-
]
|
|
2975
|
+
const weekly = accountWideWeekly
|
|
1245
2976
|
.sort(
|
|
1246
2977
|
(left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt),
|
|
1247
2978
|
)[0];
|
|
@@ -1249,18 +2980,26 @@ function buildSnapshot(context, options, titles) {
|
|
|
1249
2980
|
? (weekly.resetsAt - weekly.windowMinutes * 60) * 1_000
|
|
1250
2981
|
: null;
|
|
1251
2982
|
const completeSinceWindowStart = Boolean(
|
|
1252
|
-
|
|
2983
|
+
scope.since === null &&
|
|
2984
|
+
scope.includeArchived &&
|
|
2985
|
+
weeklyStart &&
|
|
1253
2986
|
earliestEventAt &&
|
|
1254
2987
|
Date.parse(earliestEventAt) <= weeklyStart &&
|
|
1255
|
-
context.parseErrors === 0
|
|
2988
|
+
context.parseErrors === 0 &&
|
|
2989
|
+
context.invalidTokenRecords === 0 &&
|
|
2990
|
+
context.invalidQuotaRecords === 0 &&
|
|
2991
|
+
!context.durableLedger?.sourceSummary?.sourceIncomplete &&
|
|
2992
|
+
migratedCompactedCalls === 0,
|
|
1256
2993
|
);
|
|
1257
2994
|
|
|
1258
2995
|
const notes = [
|
|
1259
2996
|
"Observed totals sum globally de-duplicated last_token_usage model-call events.",
|
|
1260
2997
|
"The snapshot keeps exact recent calls and compacts older usage into time buckets; token, cache, model, project, tool-call, and thread totals remain additive.",
|
|
1261
2998
|
"Codex thread counters are retained only as non-additive reference values because forks and subagents inherit cumulative history.",
|
|
1262
|
-
"Historical rollout files can be pruned;
|
|
2999
|
+
"Historical rollout files can be pruned; the private durable ledger keeps committed observations without making a state-only counter additive.",
|
|
1263
3000
|
"Legacy events without turn IDs use a high-specificity usage-signature heuristic and are labeled in the ledger.",
|
|
3001
|
+
"Source files are mutable inputs. Missing or tombstoned sources never delete committed observations and are surfaced in coverage.",
|
|
3002
|
+
"Migrated compacted rows preserve totals but do not invent exact event or turn identities; estimated values remain marked.",
|
|
1264
3003
|
];
|
|
1265
3004
|
|
|
1266
3005
|
return {
|
|
@@ -1269,16 +3008,48 @@ function buildSnapshot(context, options, titles) {
|
|
|
1269
3008
|
label: "Local Codex snapshot",
|
|
1270
3009
|
provenance: {
|
|
1271
3010
|
kind: "codex-local-metadata",
|
|
3011
|
+
collection: scope,
|
|
3012
|
+
sourceCutoffAt: context.sourceInventory.cutoffAt,
|
|
1272
3013
|
privacy:
|
|
1273
|
-
"Contains token metadata and Codex display titles only; credential fields, message bodies, reasoning text, tool payloads, and full local paths are not exported.
|
|
1274
|
-
|
|
1275
|
-
|
|
3014
|
+
"Contains token metadata and Codex display titles only; credential fields, message bodies, reasoning text, tool payloads, and full local paths are not exported. Path-like source labels are categorized and local path tokens in other labels are redacted; unrelated user-written title text may remain.",
|
|
3015
|
+
rateCardKind: CODEX_CREDIT_RATE_CARD_KIND,
|
|
3016
|
+
rateCardAsOf: CODEX_CREDIT_RATE_CARD_AS_OF,
|
|
3017
|
+
rateCardUrl: CODEX_CREDIT_RATE_CARD_URL,
|
|
3018
|
+
rateCardScope: CODEX_CREDIT_RATE_CARD_SCOPE,
|
|
3019
|
+
},
|
|
3020
|
+
metadata: {
|
|
3021
|
+
stateDatabase: context.stateDatabase,
|
|
3022
|
+
durableLedger: context.durableLedger
|
|
3023
|
+
? {
|
|
3024
|
+
schemaVersion: DURABLE_LEDGER_SCHEMA_VERSION,
|
|
3025
|
+
revision: context.durableLedger.revision,
|
|
3026
|
+
quotaIdentityContract: QUOTA_IDENTITY_CONTRACT_VERSION,
|
|
3027
|
+
codexHomeFingerprint: codexHomeFingerprint(context.codexHome),
|
|
3028
|
+
retentionDays: DURABLE_LEDGER_RETENTION_DAYS,
|
|
3029
|
+
compactedRetentionDays: DURABLE_LEDGER_COMPACTED_RETENTION_DAYS,
|
|
3030
|
+
legacySnapshotStatus:
|
|
3031
|
+
context.durableLedger.legacySnapshotStatus || null,
|
|
3032
|
+
legacyQuotaStatus:
|
|
3033
|
+
context.durableLedger.legacyQuotaStatus || null,
|
|
3034
|
+
legacyQuotaRowsSkipped:
|
|
3035
|
+
context.durableLedger.legacyQuotaRowsSkipped || 0,
|
|
3036
|
+
quotaContractUpgradeStatus:
|
|
3037
|
+
context.durableLedger.quotaContractUpgradeStatus || null,
|
|
3038
|
+
quotaContractRowsDiscarded:
|
|
3039
|
+
(context.durableLedger.quotaContractMigratedRowsDiscarded || 0) +
|
|
3040
|
+
(context.durableLedger.quotaContractExactRowsDiscarded || 0),
|
|
3041
|
+
}
|
|
3042
|
+
: null,
|
|
1276
3043
|
},
|
|
1277
3044
|
coverage: {
|
|
1278
3045
|
filesScanned: context.filesScanned,
|
|
1279
3046
|
bytesScanned: context.bytesScanned,
|
|
3047
|
+
filesReused: context.filesReused,
|
|
3048
|
+
bytesReused: context.bytesReused,
|
|
1280
3049
|
parseErrors: context.parseErrors,
|
|
1281
3050
|
duplicateEventsSkipped: context.duplicateEventsSkipped,
|
|
3051
|
+
invalidTokenRecords: context.invalidTokenRecords,
|
|
3052
|
+
invalidQuotaRecords: context.invalidQuotaRecords,
|
|
1282
3053
|
correctionIntervals: context.correctionIntervals,
|
|
1283
3054
|
observedTokens,
|
|
1284
3055
|
detailedTokens,
|
|
@@ -1287,10 +3058,35 @@ function buildSnapshot(context, options, titles) {
|
|
|
1287
3058
|
unresolvedThreadCounters,
|
|
1288
3059
|
legacyHeuristicEvents,
|
|
1289
3060
|
observedModelCalls,
|
|
3061
|
+
exactObservedModelCalls,
|
|
3062
|
+
migratedCompactedCalls,
|
|
3063
|
+
migratedCompactedTokens,
|
|
3064
|
+
migratedCompactedBuckets:
|
|
3065
|
+
context.durableLedger?.migratedUsageRows || 0,
|
|
3066
|
+
migratedQuotaObservations:
|
|
3067
|
+
context.durableLedger?.migratedQuotaRows || 0,
|
|
3068
|
+
compactedUsageBuckets:
|
|
3069
|
+
context.durableLedger?.compactedUsageRows || 0,
|
|
3070
|
+
legacySnapshotStatus:
|
|
3071
|
+
context.durableLedger?.legacySnapshotStatus || null,
|
|
3072
|
+
legacyQuotaStatus:
|
|
3073
|
+
context.durableLedger?.legacyQuotaStatus || null,
|
|
3074
|
+
legacyQuotaRowsSkipped:
|
|
3075
|
+
context.durableLedger?.legacyQuotaRowsSkipped || 0,
|
|
3076
|
+
sourceIncomplete: Boolean(
|
|
3077
|
+
context.durableLedger?.sourceSummary?.sourceIncomplete,
|
|
3078
|
+
),
|
|
3079
|
+
sourceStates: context.durableLedger?.sourceSummary?.counts || {
|
|
3080
|
+
active: 0,
|
|
3081
|
+
archived: 0,
|
|
3082
|
+
missing: 0,
|
|
3083
|
+
tombstoned: 0,
|
|
3084
|
+
changed: 0,
|
|
3085
|
+
},
|
|
1290
3086
|
usageBucketCount: usageStats.bucketCount,
|
|
1291
3087
|
maximumUsageResolutionSeconds: usageStats.maximumResolutionSeconds,
|
|
1292
3088
|
detailedPercent:
|
|
1293
|
-
|
|
3089
|
+
coverageTokens > 0 ? (detailedTokens / coverageTokens) * 100 : 100,
|
|
1294
3090
|
earliestEventAt,
|
|
1295
3091
|
latestEventAt,
|
|
1296
3092
|
completeSinceWindowStart,
|
|
@@ -1302,65 +3098,1062 @@ function buildSnapshot(context, options, titles) {
|
|
|
1302
3098
|
};
|
|
1303
3099
|
}
|
|
1304
3100
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
resolve(options.codexHome, "session_index.jsonl"),
|
|
1309
|
-
);
|
|
1310
|
-
const roots = [resolve(options.codexHome, "sessions")];
|
|
1311
|
-
if (options.includeArchived) {
|
|
1312
|
-
roots.push(resolve(options.codexHome, "archived_sessions"));
|
|
1313
|
-
}
|
|
1314
|
-
const files = (
|
|
1315
|
-
await Promise.all(roots.map((root) => listJsonlFiles(root)))
|
|
1316
|
-
)
|
|
1317
|
-
.flat()
|
|
1318
|
-
.sort();
|
|
1319
|
-
const sizes = await Promise.all(files.map((path) => stat(path)));
|
|
1320
|
-
const spool = await createUsageSpool();
|
|
1321
|
-
|
|
1322
|
-
const context = {
|
|
3101
|
+
function createCollectionContext(state, spool, options) {
|
|
3102
|
+
return {
|
|
3103
|
+
codexHome: options.codexHome,
|
|
1323
3104
|
stateRows: state.rows,
|
|
1324
3105
|
parents: state.parents,
|
|
3106
|
+
stateDatabase: state.metadata,
|
|
1325
3107
|
quotas: new Map(),
|
|
3108
|
+
quotaLabels: new Map(),
|
|
3109
|
+
eventSources: new Map(),
|
|
3110
|
+
eventPositions: [],
|
|
3111
|
+
callSources: new Map(),
|
|
3112
|
+
quotaSources: new Map(),
|
|
3113
|
+
quotaSourceBounds: new Map(),
|
|
3114
|
+
persistedThreads: new Map(),
|
|
3115
|
+
trustedScannedSourceIds: new Set(),
|
|
3116
|
+
durableLedger: null,
|
|
1326
3117
|
spool,
|
|
1327
3118
|
filesScanned: 0,
|
|
1328
3119
|
bytesScanned: 0,
|
|
3120
|
+
filesReused: 0,
|
|
3121
|
+
bytesReused: 0,
|
|
1329
3122
|
parseErrors: 0,
|
|
1330
3123
|
duplicateEventsSkipped: 0,
|
|
3124
|
+
invalidTokenRecords: 0,
|
|
3125
|
+
invalidQuotaRecords: 0,
|
|
1331
3126
|
correctionIntervals: 0,
|
|
3127
|
+
uncertainSourceIds: new Set(),
|
|
3128
|
+
quotaUncertainSourceIds: new Set(),
|
|
3129
|
+
// Capture the complete observed ledger even when the requested snapshot
|
|
3130
|
+
// is filtered. buildSnapshot applies the requested scope on materialize.
|
|
3131
|
+
sinceMs: -Infinity,
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
function sourceMatchesDurableCursor(entry, prior) {
|
|
3136
|
+
if (
|
|
3137
|
+
!prior ||
|
|
3138
|
+
prior.reconciliationPending ||
|
|
3139
|
+
prior.quotaReconciliationPending
|
|
3140
|
+
) return false;
|
|
3141
|
+
const size = Number(entry.size);
|
|
3142
|
+
const cursorBytes = Number(prior.cursorBytes);
|
|
3143
|
+
const device = entry.dev == null ? null : Number(entry.dev);
|
|
3144
|
+
const inode = entry.ino == null ? null : Number(entry.ino);
|
|
3145
|
+
return (
|
|
3146
|
+
Number.isSafeInteger(size) &&
|
|
3147
|
+
size >= 0 &&
|
|
3148
|
+
Number.isSafeInteger(cursorBytes) &&
|
|
3149
|
+
cursorBytes >= 0 &&
|
|
3150
|
+
cursorBytes <= size &&
|
|
3151
|
+
size === Number(prior.sizeBytes) &&
|
|
3152
|
+
Number(entry.mtimeMs) === Number(prior.mtimeMs) &&
|
|
3153
|
+
Number(entry.ctimeMs) === Number(prior.ctimeMs) &&
|
|
3154
|
+
device === prior.device &&
|
|
3155
|
+
inode === prior.inode &&
|
|
3156
|
+
prior.cursorFingerprint.length === 64
|
|
3157
|
+
);
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
function durableLedgerRelatedPaths(options) {
|
|
3161
|
+
const ledgerPath = resolveDurableLedgerPath(options);
|
|
3162
|
+
return [
|
|
3163
|
+
ledgerPath,
|
|
3164
|
+
`${ledgerPath}.writer-lock.sqlite`,
|
|
3165
|
+
`${ledgerPath}-journal`,
|
|
3166
|
+
`${ledgerPath}-wal`,
|
|
3167
|
+
`${ledgerPath}-shm`,
|
|
3168
|
+
];
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
function addSourceAssociation(map, key, sourceId) {
|
|
3172
|
+
if (!sourceId) return;
|
|
3173
|
+
const sourceIds = map.get(key) || new Set();
|
|
3174
|
+
sourceIds.add(sourceId);
|
|
3175
|
+
map.set(key, sourceIds);
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
function addQuotaSourceBounds(map, key, sourceId, timestamp) {
|
|
3179
|
+
if (!sourceId) return;
|
|
3180
|
+
const bySource = map.get(key) || new Map();
|
|
3181
|
+
const current = bySource.get(sourceId);
|
|
3182
|
+
bySource.set(sourceId, {
|
|
3183
|
+
firstSeenAt: current && current.firstSeenAt < timestamp
|
|
3184
|
+
? current.firstSeenAt
|
|
3185
|
+
: timestamp,
|
|
3186
|
+
lastSeenAt: current && current.lastSeenAt > timestamp
|
|
3187
|
+
? current.lastSeenAt
|
|
3188
|
+
: timestamp,
|
|
3189
|
+
});
|
|
3190
|
+
map.set(key, bySource);
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
function eventMetadataForToken(token, context, titles) {
|
|
3194
|
+
const { origin, occurrence, threadId } = resolvedOccurrence(token);
|
|
3195
|
+
const metadata = threadMetadata(
|
|
3196
|
+
threadId,
|
|
3197
|
+
context.stateRows,
|
|
3198
|
+
titles,
|
|
3199
|
+
context.parents,
|
|
3200
|
+
origin || occurrence,
|
|
3201
|
+
context.persistedThreads,
|
|
3202
|
+
);
|
|
3203
|
+
return {
|
|
3204
|
+
project: token.project || metadata.project,
|
|
3205
|
+
model: token.displayModel || metadata.model,
|
|
3206
|
+
source: token.sourceLabel || metadata.source,
|
|
3207
|
+
useType: token.useType || metadata.useType,
|
|
3208
|
+
rateCardModel: token.rateCardModel || occurrence.model || metadata.model,
|
|
1332
3209
|
};
|
|
3210
|
+
}
|
|
1333
3211
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
3212
|
+
function threadRecordsForLedger(context, titles) {
|
|
3213
|
+
const records = new Map();
|
|
3214
|
+
const remember = (threadId, fallback = {}) => {
|
|
3215
|
+
if (!threadId) return;
|
|
3216
|
+
const metadata = threadMetadata(
|
|
3217
|
+
threadId,
|
|
3218
|
+
context.stateRows,
|
|
3219
|
+
titles,
|
|
3220
|
+
context.parents,
|
|
3221
|
+
fallback,
|
|
3222
|
+
context.persistedThreads,
|
|
3223
|
+
);
|
|
3224
|
+
records.set(threadId, metadata);
|
|
3225
|
+
};
|
|
3226
|
+
for (const threadId of context.stateRows.keys()) remember(threadId);
|
|
3227
|
+
for (const threadId of context.parents.keys()) remember(threadId);
|
|
3228
|
+
for (const [threadId, persisted] of context.persistedThreads) {
|
|
3229
|
+
const sessionTitle = titles.get(threadId)?.title;
|
|
3230
|
+
// A title-only index change is a real metadata observation. Untouched
|
|
3231
|
+
// persisted rows remain read-only fallbacks so retention can expire them.
|
|
3232
|
+
if (sessionTitle == null) continue;
|
|
3233
|
+
const current = threadMetadata(
|
|
3234
|
+
threadId,
|
|
3235
|
+
context.stateRows,
|
|
3236
|
+
titles,
|
|
3237
|
+
context.parents,
|
|
3238
|
+
{},
|
|
3239
|
+
context.persistedThreads,
|
|
3240
|
+
);
|
|
3241
|
+
if (current.title === persisted.title) continue;
|
|
3242
|
+
remember(threadId);
|
|
3243
|
+
}
|
|
3244
|
+
for (const token of context.spool.tokenRows()) {
|
|
3245
|
+
const { origin, occurrence, threadId } = resolvedOccurrence(token);
|
|
3246
|
+
remember(threadId, origin || occurrence);
|
|
3247
|
+
}
|
|
3248
|
+
return [...records.values()];
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
const DURABLE_USAGE_FIELDS = Object.freeze([
|
|
3252
|
+
"inputTokens",
|
|
3253
|
+
"cachedInputTokens",
|
|
3254
|
+
"cacheWriteInputTokens",
|
|
3255
|
+
"outputTokens",
|
|
3256
|
+
"reasoningTokens",
|
|
3257
|
+
"totalTokens",
|
|
3258
|
+
"toolCalls",
|
|
3259
|
+
"rateCardCredits",
|
|
3260
|
+
]);
|
|
3261
|
+
const DURABLE_COUNT_FIELDS = Object.freeze([
|
|
3262
|
+
"callCount",
|
|
3263
|
+
"detailedCallCount",
|
|
3264
|
+
"inputCallCount",
|
|
3265
|
+
]);
|
|
3266
|
+
|
|
3267
|
+
function subtractCompactedRows(
|
|
3268
|
+
row,
|
|
3269
|
+
currentRowsByEventKey,
|
|
3270
|
+
currentCallKeys,
|
|
3271
|
+
) {
|
|
3272
|
+
const compactedEventKeys = row.compactedEventKeys;
|
|
3273
|
+
if (
|
|
3274
|
+
compactedEventKeys == null ||
|
|
3275
|
+
!compactedEventKeys[Symbol.iterator]
|
|
3276
|
+
) {
|
|
3277
|
+
return row;
|
|
3278
|
+
}
|
|
3279
|
+
const matches = [];
|
|
3280
|
+
for (const eventKey of compactedEventKeys) {
|
|
3281
|
+
const current = currentRowsByEventKey.get(String(eventKey));
|
|
3282
|
+
if (current) matches.push(current);
|
|
3283
|
+
}
|
|
3284
|
+
const ownedToolCallKeys = Array.isArray(row.toolCallKeys)
|
|
3285
|
+
? row.toolCallKeys.map((callKey) => String(callKey))
|
|
3286
|
+
: null;
|
|
3287
|
+
const activeToolCallKeys = ownedToolCallKeys
|
|
3288
|
+
? ownedToolCallKeys.filter((callKey) => currentCallKeys.has(callKey))
|
|
3289
|
+
: [];
|
|
3290
|
+
if (!matches.length && !activeToolCallKeys.length) return row;
|
|
3291
|
+
const residual = { ...row };
|
|
3292
|
+
for (const field of DURABLE_USAGE_FIELDS) {
|
|
3293
|
+
if (field === "rateCardCredits") {
|
|
3294
|
+
const known = matches.every((match) => match.rateCardCredits != null);
|
|
3295
|
+
residual[field] = known
|
|
3296
|
+
? Math.max(
|
|
3297
|
+
0,
|
|
3298
|
+
Number(row[field] || 0) - matches.reduce(
|
|
3299
|
+
(sum, match) => sum + Number(match.rateCardCredits || 0),
|
|
3300
|
+
0,
|
|
3301
|
+
),
|
|
3302
|
+
)
|
|
3303
|
+
: row[field];
|
|
3304
|
+
continue;
|
|
3305
|
+
}
|
|
3306
|
+
residual[field] = Math.max(
|
|
3307
|
+
0,
|
|
3308
|
+
Number(row[field] || 0) - matches.reduce(
|
|
3309
|
+
(sum, match) => sum + Number(match[field] || 0),
|
|
3310
|
+
0,
|
|
3311
|
+
),
|
|
3312
|
+
);
|
|
3313
|
+
}
|
|
3314
|
+
if (ownedToolCallKeys) {
|
|
3315
|
+
residual.toolCalls = Math.max(
|
|
3316
|
+
0,
|
|
3317
|
+
Number(row.toolCalls || 0) - activeToolCallKeys.length,
|
|
3318
|
+
);
|
|
3319
|
+
residual.toolCallKeys = ownedToolCallKeys.filter(
|
|
3320
|
+
(callKey) => !currentCallKeys.has(callKey),
|
|
3321
|
+
);
|
|
3322
|
+
}
|
|
3323
|
+
for (const field of DURABLE_COUNT_FIELDS) {
|
|
3324
|
+
residual[field] = Math.max(
|
|
3325
|
+
0,
|
|
3326
|
+
Number(row[field] || 0) - matches.reduce(
|
|
3327
|
+
(sum, match) => sum + Number(match[field] || 0),
|
|
3328
|
+
0,
|
|
3329
|
+
),
|
|
3330
|
+
);
|
|
3331
|
+
}
|
|
3332
|
+
if (residual.totalTokens <= 0 && residual.callCount <= 0) return null;
|
|
3333
|
+
const origin = row.rangeAllocationOrigin &&
|
|
3334
|
+
Object(row.rangeAllocationOrigin) === row.rangeAllocationOrigin
|
|
3335
|
+
? { ...row.rangeAllocationOrigin }
|
|
3336
|
+
: null;
|
|
3337
|
+
if (origin) {
|
|
3338
|
+
origin.inputTokens = residual.inputTokens;
|
|
3339
|
+
origin.totalTokens = residual.totalTokens;
|
|
3340
|
+
origin.callCount = residual.callCount;
|
|
3341
|
+
residual.rangeAllocationOrigin = origin;
|
|
3342
|
+
}
|
|
3343
|
+
residual.compactedEventKeys = [];
|
|
3344
|
+
return residual;
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
function createDurableSpoolSink(context) {
|
|
3348
|
+
// Keep the durable read independent from the ingest metadata map. These
|
|
3349
|
+
// lookup sets are allocated only after ingest has finished and only when a
|
|
3350
|
+
// compacted or tool row actually needs them.
|
|
3351
|
+
let currentRowsByEventKey = null;
|
|
3352
|
+
let currentCallKeys = null;
|
|
3353
|
+
const ensureCurrentCallKeys = () => {
|
|
3354
|
+
if (currentCallKeys) return;
|
|
3355
|
+
currentCallKeys = new Set();
|
|
3356
|
+
for (const call of context.spool.callRows()) {
|
|
3357
|
+
currentCallKeys.add(String(call.callKey));
|
|
3358
|
+
}
|
|
3359
|
+
};
|
|
3360
|
+
const ensureCurrentRows = () => {
|
|
3361
|
+
if (currentRowsByEventKey) return;
|
|
3362
|
+
currentRowsByEventKey = new Map();
|
|
3363
|
+
for (const current of context.spool.tokenRows()) {
|
|
3364
|
+
currentRowsByEventKey.set(String(current.eventKey), current);
|
|
3365
|
+
}
|
|
3366
|
+
ensureCurrentCallKeys();
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
function consumeUsage(storedRow) {
|
|
3370
|
+
if (storedRow.identityKind === "compacted") ensureCurrentRows();
|
|
3371
|
+
const row = storedRow.identityKind === "compacted"
|
|
3372
|
+
? subtractCompactedRows(
|
|
3373
|
+
storedRow,
|
|
3374
|
+
currentRowsByEventKey,
|
|
3375
|
+
currentCallKeys,
|
|
3376
|
+
)
|
|
3377
|
+
: repairTokenTimestamp(storedRow);
|
|
3378
|
+
if (!row) return;
|
|
3379
|
+
if (row.originThreadId) {
|
|
3380
|
+
context.spool.insertOrigin({
|
|
3381
|
+
turnId: row.turnId,
|
|
3382
|
+
threadId: row.originThreadId,
|
|
3383
|
+
timestamp: row.originTimestamp || row.timestamp,
|
|
3384
|
+
deltaMs: 0,
|
|
3385
|
+
model: row.originModel || row.model,
|
|
3386
|
+
effort: row.originEffort || row.effort,
|
|
3387
|
+
cwd: row.originCwd || row.cwd,
|
|
3388
|
+
gitOrigin: row.originGitOrigin || row.gitOrigin,
|
|
3389
|
+
rawSource: row.originRawSource || row.rawSource,
|
|
3390
|
+
serviceTier: row.originServiceTier || row.serviceTier,
|
|
3391
|
+
});
|
|
3392
|
+
}
|
|
3393
|
+
const occurrence = {
|
|
3394
|
+
threadId: row.threadId,
|
|
3395
|
+
timestamp: row.timestamp,
|
|
3396
|
+
model: row.model,
|
|
3397
|
+
effort: row.effort,
|
|
3398
|
+
cwd: row.cwd,
|
|
3399
|
+
gitOrigin: row.gitOrigin,
|
|
3400
|
+
rawSource: row.rawSource,
|
|
3401
|
+
serviceTier: row.serviceTier,
|
|
3402
|
+
};
|
|
3403
|
+
context.spool.insertToken(
|
|
3404
|
+
row.eventKey || row.observationId,
|
|
3405
|
+
row.turnId,
|
|
3406
|
+
{
|
|
3407
|
+
inputTokens: row.inputTokens,
|
|
3408
|
+
cachedInputTokens: row.cachedInputTokens,
|
|
3409
|
+
cacheWriteInputTokens: row.cacheWriteInputTokens,
|
|
3410
|
+
outputTokens: row.outputTokens,
|
|
3411
|
+
reasoningTokens: row.reasoningTokens,
|
|
3412
|
+
totalTokens: row.totalTokens,
|
|
3413
|
+
toolCalls: row.identityKind === "exact" ? 0 : row.toolCalls,
|
|
3414
|
+
componentsValid: row.componentsValid,
|
|
3415
|
+
},
|
|
3416
|
+
occurrence,
|
|
3417
|
+
true,
|
|
3418
|
+
{
|
|
3419
|
+
project: row.project,
|
|
3420
|
+
displayModel: row.displayModel,
|
|
3421
|
+
source: row.source,
|
|
3422
|
+
useType: row.useType,
|
|
3423
|
+
rateCardModel: row.rateCardModel,
|
|
3424
|
+
rateCardCredits: row.rateCardCredits,
|
|
3425
|
+
identityKind: row.identityKind,
|
|
3426
|
+
rangeAllocationEstimated: row.rangeAllocationEstimated,
|
|
3427
|
+
rangeAllocationOrigin: row.rangeAllocationOrigin,
|
|
3428
|
+
callCount: row.callCount,
|
|
3429
|
+
detailedCallCount: row.detailedCallCount,
|
|
3430
|
+
inputCallCount: row.inputCallCount,
|
|
3431
|
+
},
|
|
3432
|
+
);
|
|
3433
|
+
if (row.identityKind === "exact") {
|
|
3434
|
+
context.spool.restoreTokenPlacement(
|
|
3435
|
+
row.eventKey || row.observationId,
|
|
3436
|
+
occurrence,
|
|
3437
|
+
{
|
|
3438
|
+
rangeAllocationEstimated: row.rangeAllocationEstimated,
|
|
3439
|
+
rangeAllocationOrigin: row.rangeAllocationOrigin,
|
|
3440
|
+
},
|
|
3441
|
+
);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
function consumeTool(call) {
|
|
3446
|
+
ensureCurrentCallKeys();
|
|
3447
|
+
if (!call.usageOwned) return;
|
|
3448
|
+
const hasUnscannedSource = [...call.sourceIds].some(
|
|
3449
|
+
(sourceId) => !context.trustedScannedSourceIds.has(String(sourceId)),
|
|
3450
|
+
);
|
|
3451
|
+
if (!hasUnscannedSource) return;
|
|
3452
|
+
context.spool.insertCall(
|
|
3453
|
+
call.callKey,
|
|
3454
|
+
call.turnId,
|
|
3455
|
+
call.threadId,
|
|
3456
|
+
call.originalLikely,
|
|
3457
|
+
);
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
function addMetadata(ledger) {
|
|
3461
|
+
for (const quota of ledger.quotaRows) {
|
|
3462
|
+
const key = durableQuotaObservationKey({
|
|
3463
|
+
limitKey: quota.limitKey,
|
|
3464
|
+
windowMinutes: quota.windowMinutes,
|
|
3465
|
+
resetsAt: quota.resetsAt,
|
|
3466
|
+
usedPercent: quota.usedPercent,
|
|
3467
|
+
});
|
|
3468
|
+
context.quotas.set(key, quota);
|
|
3469
|
+
}
|
|
3470
|
+
for (const row of ledger.threadRows) {
|
|
3471
|
+
context.persistedThreads.set(row.id, row);
|
|
3472
|
+
if (row.parentThreadId && !context.parents.has(row.id)) {
|
|
3473
|
+
context.parents.set(row.id, row.parentThreadId);
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
context.durableLedger = ledger;
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
return { consumeUsage, consumeTool, addMetadata };
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
function reduceScanResult(result, context, inventorySourceId = null) {
|
|
3483
|
+
const sourceId = inventorySourceId ||
|
|
3484
|
+
durableSourceId(context.codexHome, result.path);
|
|
3485
|
+
const uncertain = result.parseErrors > 0 || result.invalidTokenRecords > 0;
|
|
3486
|
+
const quotaUncertain = uncertain || result.invalidQuotaRecords > 0;
|
|
3487
|
+
if (quotaUncertain) {
|
|
3488
|
+
context.quotaUncertainSourceIds.add(String(sourceId));
|
|
3489
|
+
}
|
|
3490
|
+
// A source-local omission makes every operation from that scan unsafe: a
|
|
3491
|
+
// shifted ordinal can overwrite an older observation, and partial calls can
|
|
3492
|
+
// reassign ownership even when membership pruning is disabled. Retain the
|
|
3493
|
+
// coverage evidence and source watermark, but let the durable ledger keep
|
|
3494
|
+
// the last complete interpretation until this source scans cleanly.
|
|
3495
|
+
if (uncertain) {
|
|
3496
|
+
context.uncertainSourceIds.add(String(sourceId));
|
|
3497
|
+
context.parseErrors += result.parseErrors;
|
|
3498
|
+
context.invalidTokenRecords += result.invalidTokenRecords || 0;
|
|
3499
|
+
context.invalidQuotaRecords += result.invalidQuotaRecords || 0;
|
|
3500
|
+
context.correctionIntervals += result.correctionIntervals;
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
context.trustedScannedSourceIds.add(String(sourceId));
|
|
3504
|
+
let currentCandidate = null;
|
|
3505
|
+
let currentCandidateSelected = false;
|
|
3506
|
+
for (const operation of result.operations) {
|
|
3507
|
+
if (operation.kind === "parent") {
|
|
3508
|
+
context.parents.set(operation.threadId, operation.parentThreadId);
|
|
3509
|
+
continue;
|
|
3510
|
+
}
|
|
3511
|
+
if (operation.kind === "origin") {
|
|
3512
|
+
currentCandidate = operation.candidate;
|
|
3513
|
+
currentCandidateSelected = context.spool.insertOrigin(currentCandidate);
|
|
3514
|
+
continue;
|
|
3515
|
+
}
|
|
3516
|
+
if (operation.kind === "origin_update") {
|
|
3517
|
+
if (currentCandidate?.turnId === operation.turnId) {
|
|
3518
|
+
currentCandidate.model = operation.model;
|
|
3519
|
+
currentCandidate.effort = operation.effort;
|
|
3520
|
+
currentCandidate.cwd = operation.cwd;
|
|
3521
|
+
if (currentCandidateSelected) {
|
|
3522
|
+
context.spool.updateOrigin(currentCandidate);
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
continue;
|
|
3526
|
+
}
|
|
3527
|
+
if (operation.kind === "quota") {
|
|
3528
|
+
for (const candidate of operation.candidates) {
|
|
3529
|
+
if (!quotaUncertain) {
|
|
3530
|
+
rememberQuotaLabel(context.quotaLabels, sourceId, candidate);
|
|
3531
|
+
}
|
|
3532
|
+
rememberQuota(context.quotas, candidate, context.sinceMs);
|
|
3533
|
+
addSourceAssociation(
|
|
3534
|
+
context.quotaSources,
|
|
3535
|
+
candidate.observationKey,
|
|
3536
|
+
sourceId,
|
|
3537
|
+
);
|
|
3538
|
+
addQuotaSourceBounds(
|
|
3539
|
+
context.quotaSourceBounds,
|
|
3540
|
+
candidate.observationKey,
|
|
3541
|
+
sourceId,
|
|
3542
|
+
candidate.timestamp,
|
|
3543
|
+
);
|
|
3544
|
+
}
|
|
3545
|
+
continue;
|
|
3546
|
+
}
|
|
3547
|
+
if (operation.kind === "call") {
|
|
3548
|
+
context.spool.insertCall(
|
|
3549
|
+
operation.callKey,
|
|
3550
|
+
operation.turnId,
|
|
3551
|
+
operation.threadId,
|
|
3552
|
+
operation.originalLikely,
|
|
3553
|
+
);
|
|
3554
|
+
addSourceAssociation(context.callSources, operation.callKey, sourceId);
|
|
3555
|
+
continue;
|
|
3556
|
+
}
|
|
3557
|
+
if (operation.kind === "token") {
|
|
3558
|
+
const inserted = context.spool.insertToken(
|
|
3559
|
+
operation.eventKey,
|
|
3560
|
+
operation.turnId,
|
|
3561
|
+
operation.usage,
|
|
3562
|
+
operation.occurrence,
|
|
3563
|
+
operation.originalLikely,
|
|
3564
|
+
);
|
|
3565
|
+
if (!inserted) context.duplicateEventsSkipped += 1;
|
|
3566
|
+
addSourceAssociation(context.eventSources, operation.eventKey, sourceId);
|
|
3567
|
+
if (operation.eventOrdinal !== null && operation.eventOrdinal !== undefined) {
|
|
3568
|
+
context.eventPositions.push({
|
|
3569
|
+
sourceId,
|
|
3570
|
+
ordinal: operation.eventOrdinal,
|
|
3571
|
+
eventKey: operation.eventKey,
|
|
1348
3572
|
});
|
|
1349
3573
|
}
|
|
3574
|
+
continue;
|
|
3575
|
+
}
|
|
3576
|
+
throw new Error(`Unknown rollout operation: ${operation.kind}`);
|
|
3577
|
+
}
|
|
3578
|
+
context.parseErrors += result.parseErrors;
|
|
3579
|
+
context.invalidTokenRecords += result.invalidTokenRecords || 0;
|
|
3580
|
+
context.invalidQuotaRecords += result.invalidQuotaRecords || 0;
|
|
3581
|
+
context.correctionIntervals += result.correctionIntervals;
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
function createProgressReporter(files, onProgress) {
|
|
3585
|
+
let completed = 0;
|
|
3586
|
+
return async (index) => {
|
|
3587
|
+
completed += 1;
|
|
3588
|
+
if (
|
|
3589
|
+
completed === 1 ||
|
|
3590
|
+
completed === files.length ||
|
|
3591
|
+
completed % 10 === 0
|
|
3592
|
+
) {
|
|
3593
|
+
await onProgress({
|
|
3594
|
+
current: completed,
|
|
3595
|
+
total: files.length,
|
|
3596
|
+
path: files[index],
|
|
3597
|
+
});
|
|
3598
|
+
}
|
|
3599
|
+
};
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
function workerError(error, terminal) {
|
|
3603
|
+
const result = new Error(error?.message || "Rollout worker failed.");
|
|
3604
|
+
result.name = error?.name || "Error";
|
|
3605
|
+
if (terminal) result.rolloutWorkerFailure = true;
|
|
3606
|
+
if (error?.code) result.code = error.code;
|
|
3607
|
+
if (error?.stack) result.stack = error.stack;
|
|
3608
|
+
return result;
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
async function scanFilesSequential(
|
|
3612
|
+
files,
|
|
3613
|
+
stateRows,
|
|
3614
|
+
continuityBytes,
|
|
3615
|
+
maximumBytes,
|
|
3616
|
+
sourceIdentities,
|
|
3617
|
+
onProgress,
|
|
3618
|
+
onResult,
|
|
3619
|
+
) {
|
|
3620
|
+
const reportCompleted = createProgressReporter(files, onProgress);
|
|
3621
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
3622
|
+
const result = await scanRollout(
|
|
3623
|
+
files[index],
|
|
3624
|
+
stateRows,
|
|
3625
|
+
undefined,
|
|
3626
|
+
continuityBytes[index],
|
|
3627
|
+
maximumBytes[index],
|
|
3628
|
+
sourceIdentities[index],
|
|
3629
|
+
);
|
|
3630
|
+
await onResult(result, index);
|
|
3631
|
+
await reportCompleted(index);
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
export function rolloutWorkerStateEntry(path, stateRows) {
|
|
3636
|
+
const threadId = String(path).match(UUID_AT_END)?.[1];
|
|
3637
|
+
if (!threadId) return null;
|
|
3638
|
+
const row = stateRows.get(threadId);
|
|
3639
|
+
if (!row) return null;
|
|
3640
|
+
// A rollout scan needs only these five enrichment fields. In particular,
|
|
3641
|
+
// do not clone titles (which may contain large user-authored text) or the
|
|
3642
|
+
// rest of the state database into every worker isolate.
|
|
3643
|
+
return [threadId, {
|
|
3644
|
+
model: row.model,
|
|
3645
|
+
reasoning_effort: row.reasoning_effort,
|
|
3646
|
+
cwd: row.cwd,
|
|
3647
|
+
git_origin_url: row.git_origin_url,
|
|
3648
|
+
source: row.source,
|
|
3649
|
+
}];
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
async function scanFilesWithWorkers(
|
|
3653
|
+
files,
|
|
3654
|
+
stateRows,
|
|
3655
|
+
continuityBytes,
|
|
3656
|
+
maximumBytes,
|
|
3657
|
+
sourceIdentities,
|
|
3658
|
+
concurrency,
|
|
3659
|
+
onProgress,
|
|
3660
|
+
onResult,
|
|
3661
|
+
) {
|
|
3662
|
+
const reportCompleted = createProgressReporter(files, onProgress);
|
|
3663
|
+
const workerCount = Math.min(concurrency, files.length);
|
|
3664
|
+
const maxReorderWindow = Math.max(1, concurrency * 2);
|
|
3665
|
+
const workers = [];
|
|
3666
|
+
try {
|
|
3667
|
+
for (let index = 0; index < workerCount; index += 1) {
|
|
3668
|
+
workers.push(
|
|
3669
|
+
new Worker(new URL(import.meta.url), {
|
|
3670
|
+
type: "module",
|
|
3671
|
+
workerData: {
|
|
3672
|
+
tokenLedgerImporterWorker: true,
|
|
3673
|
+
},
|
|
3674
|
+
execArgv: ["--no-warnings"],
|
|
3675
|
+
}),
|
|
3676
|
+
);
|
|
3677
|
+
}
|
|
3678
|
+
} catch (error) {
|
|
3679
|
+
await Promise.all(workers.map((worker) => worker.terminate()));
|
|
3680
|
+
throw error;
|
|
3681
|
+
}
|
|
3682
|
+
|
|
3683
|
+
let nextIndex = 0;
|
|
3684
|
+
let completedCount = 0;
|
|
3685
|
+
let closed = false;
|
|
3686
|
+
const assignments = new Map();
|
|
3687
|
+
const pendingResults = new Map();
|
|
3688
|
+
let nextResultIndex = 0;
|
|
3689
|
+
let resolveDone;
|
|
3690
|
+
let rejectDone;
|
|
3691
|
+
const done = new Promise((resolve, reject) => {
|
|
3692
|
+
resolveDone = resolve;
|
|
3693
|
+
rejectDone = reject;
|
|
3694
|
+
});
|
|
3695
|
+
|
|
3696
|
+
// Progress callbacks may be async; serialize them, dispatch further files
|
|
3697
|
+
// only after each settles, and hold completion open until the queue drains,
|
|
3698
|
+
// so a rejection fails the scan instead of becoming an unhandled rejection.
|
|
3699
|
+
let progressChain = Promise.resolve();
|
|
3700
|
+
function queueProgress(index) {
|
|
3701
|
+
progressChain = progressChain
|
|
3702
|
+
.then(() => reportCompleted(index))
|
|
3703
|
+
.then(() => {
|
|
3704
|
+
if (!closed) dispatchAvailable();
|
|
3705
|
+
});
|
|
3706
|
+
progressChain.catch((error) => close(error));
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
function close(error = null) {
|
|
3710
|
+
if (closed) return;
|
|
3711
|
+
closed = true;
|
|
3712
|
+
Promise.all([
|
|
3713
|
+
Promise.all(workers.map((worker) => worker.terminate())),
|
|
3714
|
+
progressChain,
|
|
3715
|
+
]).then(
|
|
3716
|
+
() => {
|
|
3717
|
+
if (error) rejectDone(error);
|
|
3718
|
+
else resolveDone();
|
|
3719
|
+
},
|
|
3720
|
+
(cleanupError) => rejectDone(error || cleanupError),
|
|
3721
|
+
);
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
function dispatch(worker) {
|
|
3725
|
+
if (closed || nextIndex >= files.length) return;
|
|
3726
|
+
const index = nextIndex;
|
|
3727
|
+
nextIndex += 1;
|
|
3728
|
+
assignments.set(worker, { index });
|
|
3729
|
+
try {
|
|
3730
|
+
worker.postMessage({
|
|
3731
|
+
index,
|
|
3732
|
+
path: files[index],
|
|
3733
|
+
continuityBytes: continuityBytes[index],
|
|
3734
|
+
maximumBytes: maximumBytes[index],
|
|
3735
|
+
expectedIdentity: sourceIdentities[index],
|
|
3736
|
+
stateEntry: rolloutWorkerStateEntry(files[index], stateRows),
|
|
3737
|
+
});
|
|
3738
|
+
} catch (error) {
|
|
3739
|
+
close(error);
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
function flushResults() {
|
|
3744
|
+
while (pendingResults.has(nextResultIndex)) {
|
|
3745
|
+
const result = pendingResults.get(nextResultIndex);
|
|
3746
|
+
pendingResults.delete(nextResultIndex);
|
|
3747
|
+
onResult(result, nextResultIndex);
|
|
3748
|
+
nextResultIndex += 1;
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
function dispatchAvailable() {
|
|
3753
|
+
while (
|
|
3754
|
+
!closed &&
|
|
3755
|
+
nextIndex < files.length &&
|
|
3756
|
+
nextIndex - nextResultIndex < maxReorderWindow
|
|
3757
|
+
) {
|
|
3758
|
+
const worker = workers.find((candidate) => !assignments.has(candidate));
|
|
3759
|
+
if (!worker) return;
|
|
3760
|
+
dispatch(worker);
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
for (const worker of workers) {
|
|
3765
|
+
worker.on("message", (message) => {
|
|
3766
|
+
if (closed) return;
|
|
3767
|
+
const assignment = assignments.get(worker);
|
|
3768
|
+
if (assignment?.index !== message.index) {
|
|
3769
|
+
close(new Error("Rollout worker returned an unexpected file index."));
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3772
|
+
assignments.delete(worker);
|
|
3773
|
+
if (message.error) {
|
|
3774
|
+
// Filesystem mutation errors stay retryable regardless of how much
|
|
3775
|
+
// of the queue already completed, so pruning mid-scan re-inventories
|
|
3776
|
+
// instead of aborting the refresh.
|
|
3777
|
+
close(
|
|
3778
|
+
workerError(
|
|
3779
|
+
message.error,
|
|
3780
|
+
!SOURCE_MUTATION_ERROR_CODES.includes(message.error?.code),
|
|
3781
|
+
),
|
|
3782
|
+
);
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3785
|
+
pendingResults.set(message.index, message.result);
|
|
3786
|
+
try {
|
|
3787
|
+
flushResults();
|
|
3788
|
+
} catch (error) {
|
|
3789
|
+
close(error);
|
|
3790
|
+
return;
|
|
3791
|
+
}
|
|
3792
|
+
queueProgress(message.index);
|
|
3793
|
+
completedCount += 1;
|
|
3794
|
+
if (completedCount === files.length) close();
|
|
3795
|
+
});
|
|
3796
|
+
worker.on("error", (error) => close(error));
|
|
3797
|
+
worker.on("exit", (code) => {
|
|
3798
|
+
if (!closed) {
|
|
3799
|
+
close(
|
|
3800
|
+
new Error(
|
|
3801
|
+
`Rollout worker exited before completing its scan (code ${code}).`,
|
|
3802
|
+
),
|
|
3803
|
+
);
|
|
3804
|
+
}
|
|
3805
|
+
});
|
|
3806
|
+
}
|
|
3807
|
+
dispatchAvailable();
|
|
3808
|
+
return done;
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
async function scanFiles(
|
|
3812
|
+
files,
|
|
3813
|
+
stateRows,
|
|
3814
|
+
continuityBytes,
|
|
3815
|
+
maximumBytes,
|
|
3816
|
+
sourceIdentities,
|
|
3817
|
+
concurrency,
|
|
3818
|
+
onProgress,
|
|
3819
|
+
onResult,
|
|
3820
|
+
) {
|
|
3821
|
+
if (!files.length) return;
|
|
3822
|
+
if (concurrency <= 1) {
|
|
3823
|
+
return scanFilesSequential(
|
|
3824
|
+
files,
|
|
3825
|
+
stateRows,
|
|
3826
|
+
continuityBytes,
|
|
3827
|
+
maximumBytes,
|
|
3828
|
+
sourceIdentities,
|
|
3829
|
+
onProgress,
|
|
3830
|
+
onResult,
|
|
3831
|
+
);
|
|
3832
|
+
}
|
|
3833
|
+
return scanFilesWithWorkers(
|
|
3834
|
+
files,
|
|
3835
|
+
stateRows,
|
|
3836
|
+
continuityBytes,
|
|
3837
|
+
maximumBytes,
|
|
3838
|
+
sourceIdentities,
|
|
3839
|
+
concurrency,
|
|
3840
|
+
onProgress,
|
|
3841
|
+
onResult,
|
|
3842
|
+
);
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3845
|
+
async function collectUsageWithConcurrency(
|
|
3846
|
+
options,
|
|
3847
|
+
inventory,
|
|
3848
|
+
onProgress,
|
|
3849
|
+
concurrency,
|
|
3850
|
+
reuseUnchanged = true,
|
|
3851
|
+
) {
|
|
3852
|
+
const state = await readState(options.codexHome);
|
|
3853
|
+
const titles = await readSessionTitles(
|
|
3854
|
+
resolve(options.codexHome, "session_index.jsonl"),
|
|
3855
|
+
);
|
|
3856
|
+
const spool = await createUsageSpool();
|
|
3857
|
+
const context = createCollectionContext(state, spool, options);
|
|
3858
|
+
context.sourceInventory = inventory;
|
|
3859
|
+
const durableContinuity = await readDurableSourceContinuity(
|
|
3860
|
+
resolveDurableLedgerPath(options),
|
|
3861
|
+
{
|
|
3862
|
+
codexHome: options.codexHome,
|
|
3863
|
+
inventory,
|
|
3864
|
+
},
|
|
3865
|
+
);
|
|
3866
|
+
const priorContinuity = durableContinuity.sources;
|
|
3867
|
+
if (durableContinuity.threadRecords instanceof Map) {
|
|
3868
|
+
for (const [threadId, row] of durableContinuity.threadRecords) {
|
|
3869
|
+
context.persistedThreads.set(threadId, row);
|
|
1350
3870
|
}
|
|
1351
|
-
|
|
3871
|
+
}
|
|
3872
|
+
const threadMetadataFingerprints = currentThreadMetadataFingerprints(state);
|
|
3873
|
+
const metadataChanged = !sourceWatermarksEqual(
|
|
3874
|
+
durableContinuity.metadataWatermark,
|
|
3875
|
+
inventory.metadataWatermark,
|
|
3876
|
+
);
|
|
3877
|
+
const changedMetadataThreadIds = metadataChanged
|
|
3878
|
+
? metadataChangedThreadIds({
|
|
3879
|
+
state,
|
|
3880
|
+
titles,
|
|
3881
|
+
currentFingerprints: threadMetadataFingerprints,
|
|
3882
|
+
previousFingerprints: durableContinuity.threadMetadataFingerprints,
|
|
3883
|
+
persistedThreads: durableContinuity.threadRecords,
|
|
3884
|
+
})
|
|
3885
|
+
: new Set();
|
|
3886
|
+
const scanIndexes = [];
|
|
3887
|
+
for (let index = 0; index < inventory.files.length; index += 1) {
|
|
3888
|
+
const entry = inventory.files[index];
|
|
3889
|
+
const prior = priorContinuity.get(String(entry.sourceId));
|
|
3890
|
+
const threadId = entry.path.match(UUID_AT_END)?.[1] || null;
|
|
3891
|
+
const metadataRequiresScan = metadataChanged && (
|
|
3892
|
+
changedMetadataThreadIds === null ||
|
|
3893
|
+
threadId === null ||
|
|
3894
|
+
changedMetadataThreadIds.has(threadId)
|
|
3895
|
+
);
|
|
3896
|
+
if (
|
|
3897
|
+
reuseUnchanged &&
|
|
3898
|
+
sourceMatchesDurableCursor(entry, prior) &&
|
|
3899
|
+
!metadataRequiresScan
|
|
3900
|
+
) {
|
|
3901
|
+
entry.cursorFingerprint = prior.cursorFingerprint;
|
|
3902
|
+
entry.scanBytes = prior.cursorBytes;
|
|
3903
|
+
entry.continuityBytes = prior.cursorBytes;
|
|
3904
|
+
entry.continuityFingerprint = prior.cursorFingerprint;
|
|
3905
|
+
context.filesReused += 1;
|
|
3906
|
+
context.bytesReused += entry.size;
|
|
3907
|
+
} else {
|
|
3908
|
+
scanIndexes.push(index);
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
const files = scanIndexes.map((index) => inventory.files[index].path);
|
|
3912
|
+
const continuityBytes = scanIndexes.map((index) =>
|
|
3913
|
+
priorContinuity.get(String(inventory.files[index].sourceId))?.cursorBytes ?? null
|
|
3914
|
+
);
|
|
3915
|
+
const maximumBytes = [];
|
|
3916
|
+
for (const index of scanIndexes) {
|
|
3917
|
+
const entry = inventory.files[index];
|
|
3918
|
+
const scanBytes = await scanCutoffForFile(entry);
|
|
3919
|
+
entry.scanBytes = scanBytes;
|
|
3920
|
+
maximumBytes.push(scanBytes);
|
|
3921
|
+
}
|
|
3922
|
+
const sourceIdentities = scanIndexes.map((index) => ({
|
|
3923
|
+
device: inventory.files[index].dev,
|
|
3924
|
+
inode: inventory.files[index].ino,
|
|
3925
|
+
}));
|
|
3926
|
+
let publicationWatermark = inventory.watermark;
|
|
3927
|
+
let stagedWatermark = null;
|
|
3928
|
+
|
|
3929
|
+
try {
|
|
3930
|
+
await scanFiles(
|
|
3931
|
+
files,
|
|
3932
|
+
state.rows,
|
|
3933
|
+
continuityBytes,
|
|
3934
|
+
maximumBytes,
|
|
3935
|
+
sourceIdentities,
|
|
3936
|
+
concurrency,
|
|
3937
|
+
onProgress,
|
|
3938
|
+
(result, index) => {
|
|
3939
|
+
const inventoryIndex = scanIndexes[index];
|
|
3940
|
+
inventory.files[inventoryIndex].cursorFingerprint = result.cursorFingerprint;
|
|
3941
|
+
inventory.files[inventoryIndex].continuityBytes = result.continuityBytes;
|
|
3942
|
+
inventory.files[inventoryIndex].continuityFingerprint =
|
|
3943
|
+
result.continuityFingerprint;
|
|
3944
|
+
reduceScanResult(
|
|
3945
|
+
result,
|
|
3946
|
+
context,
|
|
3947
|
+
inventory.files[inventoryIndex]?.sourceId,
|
|
3948
|
+
);
|
|
3949
|
+
context.filesScanned += 1;
|
|
3950
|
+
context.bytesScanned += inventory.files[inventoryIndex].scanBytes ??
|
|
3951
|
+
inventory.files[inventoryIndex].size;
|
|
3952
|
+
},
|
|
3953
|
+
);
|
|
3954
|
+
const after = await sourceInventory(
|
|
3955
|
+
options.codexHome,
|
|
3956
|
+
options.includeArchived,
|
|
3957
|
+
);
|
|
3958
|
+
// Every rollout worker read a bounded byte range captured by `inventory`.
|
|
3959
|
+
// Once all reads complete, that dataset is a valid point-in-time cutoff;
|
|
3960
|
+
// source changes observed afterward belong to the next refresh and must
|
|
3961
|
+
// not force a complete rescan of the already-captured files.
|
|
3962
|
+
publicationWatermark = acceptedSourceWatermark(inventory, after);
|
|
3963
|
+
context.spool.finishWrites();
|
|
3964
|
+
// Durable reconciliation makes several sequential passes. Give each pass
|
|
3965
|
+
// a fresh anchored SQLite iterator instead of retaining every spool row in
|
|
3966
|
+
// the parent JavaScript heap for the lifetime of the refresh.
|
|
3967
|
+
const tokenRows = {
|
|
3968
|
+
[Symbol.iterator]: () => context.spool.tokenRows(),
|
|
3969
|
+
};
|
|
3970
|
+
const callRows = {
|
|
3971
|
+
[Symbol.iterator]: () => context.spool.callRows(),
|
|
3972
|
+
};
|
|
3973
|
+
const durableSpool = createDurableSpoolSink(context);
|
|
3974
|
+
const eventMetadata = new Map();
|
|
3975
|
+
for (const token of tokenRows) {
|
|
3976
|
+
eventMetadata.set(
|
|
3977
|
+
String(token.eventKey),
|
|
3978
|
+
eventMetadataForToken(token, context, titles),
|
|
3979
|
+
);
|
|
3980
|
+
}
|
|
3981
|
+
const validateSources = async () => {
|
|
3982
|
+
// A completed bounded scan is the staged CLI candidate's point-in-time
|
|
3983
|
+
// cutoff. Source activity after those reads cannot change its contents;
|
|
3984
|
+
// publish once and reconcile later activity on the next refresh.
|
|
3985
|
+
if (options.stageSnapshot instanceof Function) {
|
|
3986
|
+
publicationWatermark = stagedWatermark ?? publicationWatermark;
|
|
3987
|
+
return;
|
|
3988
|
+
}
|
|
3989
|
+
const finalInventory = await sourceInventory(
|
|
3990
|
+
options.codexHome,
|
|
3991
|
+
options.includeArchived,
|
|
3992
|
+
);
|
|
3993
|
+
if (!(await sourceInventoryPreservesCutoff(
|
|
3994
|
+
inventory,
|
|
3995
|
+
finalInventory,
|
|
3996
|
+
))) {
|
|
3997
|
+
const error = new Error(
|
|
3998
|
+
"Local Codex sources changed during collection; the candidate snapshot was not published.",
|
|
3999
|
+
);
|
|
4000
|
+
error.code = "ERR_SOURCE_CHANGED_DURING_COLLECTION";
|
|
4001
|
+
throw error;
|
|
4002
|
+
}
|
|
4003
|
+
const acceptedWatermark = acceptedSourceWatermark(
|
|
4004
|
+
inventory,
|
|
4005
|
+
finalInventory,
|
|
4006
|
+
);
|
|
4007
|
+
// Rollout integrity is checked against the captured cutoff above. Once a
|
|
4008
|
+
// snapshot is staged, retain that accepted watermark: Codex may keep
|
|
4009
|
+
// appending advisory SQLite/session metadata while the ledger commits,
|
|
4010
|
+
// and that activity must not discard an otherwise valid snapshot.
|
|
4011
|
+
publicationWatermark = stagedWatermark ?? acceptedWatermark;
|
|
4012
|
+
};
|
|
4013
|
+
let stagedSnapshot = null;
|
|
4014
|
+
const stageCandidate = async ({ ledger }) => {
|
|
4015
|
+
if (!(options.stageSnapshot instanceof Function)) return null;
|
|
4016
|
+
durableSpool.addMetadata(ledger);
|
|
4017
|
+
const candidate = {
|
|
4018
|
+
...buildSnapshot(context, options, titles),
|
|
4019
|
+
sourceWatermark: publicationWatermark,
|
|
4020
|
+
};
|
|
4021
|
+
stagedWatermark = publicationWatermark;
|
|
4022
|
+
stagedSnapshot = await options.stageSnapshot(candidate);
|
|
4023
|
+
return stagedSnapshot;
|
|
4024
|
+
};
|
|
4025
|
+
const ledger = await updateDurableLedger({
|
|
4026
|
+
options,
|
|
4027
|
+
codexHome: options.codexHome,
|
|
4028
|
+
inventory,
|
|
4029
|
+
includeArchived: options.includeArchived,
|
|
4030
|
+
tokenRows,
|
|
4031
|
+
callRows,
|
|
4032
|
+
quotas: [...context.quotas.values()],
|
|
4033
|
+
eventSources: context.eventSources,
|
|
4034
|
+
eventPositions: context.eventPositions,
|
|
4035
|
+
callSources: context.callSources,
|
|
4036
|
+
quotaSources: context.quotaSources,
|
|
4037
|
+
quotaSourceBounds: context.quotaSourceBounds,
|
|
4038
|
+
quotaLabelEvidence: [...context.quotaLabels.values()].map((evidence) => ({
|
|
4039
|
+
sourceId: evidence.sourceId,
|
|
4040
|
+
limitKey: evidence.limitKey,
|
|
4041
|
+
limitName: evidence.limitName,
|
|
4042
|
+
observedAt: evidence.observedAt,
|
|
4043
|
+
})),
|
|
4044
|
+
uncertainSourceIds: context.uncertainSourceIds,
|
|
4045
|
+
quotaUncertainSourceIds: context.quotaUncertainSourceIds,
|
|
4046
|
+
eventMetadata,
|
|
4047
|
+
threadRecords: threadRecordsForLedger(context, titles),
|
|
4048
|
+
threadMetadataFingerprints,
|
|
4049
|
+
nowMs: Date.now(),
|
|
4050
|
+
faultInjector: options.faultInjector,
|
|
4051
|
+
validateBeforeCommit: validateSources,
|
|
4052
|
+
validateAfterCommit: validateSources,
|
|
4053
|
+
stageBeforeCommit: stageCandidate,
|
|
4054
|
+
onMaterializedRow: ({ kind, row }) => {
|
|
4055
|
+
if (kind === "usage") durableSpool.consumeUsage(row);
|
|
4056
|
+
else if (kind === "tool") durableSpool.consumeTool(row);
|
|
4057
|
+
},
|
|
4058
|
+
});
|
|
4059
|
+
if (stagedSnapshot) return stagedSnapshot.snapshot;
|
|
4060
|
+
durableSpool.addMetadata(ledger);
|
|
4061
|
+
return {
|
|
4062
|
+
...buildSnapshot(context, options, titles),
|
|
4063
|
+
sourceWatermark: publicationWatermark,
|
|
4064
|
+
};
|
|
1352
4065
|
} finally {
|
|
1353
4066
|
spool.close();
|
|
1354
4067
|
await rm(spool.directory, { recursive: true, force: true });
|
|
1355
4068
|
}
|
|
1356
4069
|
}
|
|
1357
4070
|
|
|
4071
|
+
async function collectUsageAttempt(options, inventory, onProgress) {
|
|
4072
|
+
return collectUsageWithConcurrency(
|
|
4073
|
+
options,
|
|
4074
|
+
inventory,
|
|
4075
|
+
onProgress,
|
|
4076
|
+
SCAN_CONCURRENCY,
|
|
4077
|
+
);
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
function isSourceMutationError(error) {
|
|
4081
|
+
return (
|
|
4082
|
+
!error?.rolloutWorkerFailure &&
|
|
4083
|
+
(
|
|
4084
|
+
SOURCE_MUTATION_ERROR_CODES.includes(error?.code) ||
|
|
4085
|
+
error?.code === "ERR_SOURCE_CHANGED_DURING_COLLECTION"
|
|
4086
|
+
)
|
|
4087
|
+
);
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
export async function collectUsage(options, onProgress = () => {}) {
|
|
4091
|
+
for (let attempt = 1; attempt <= SOURCE_COLLECTION_MAX_ATTEMPTS; attempt += 1) {
|
|
4092
|
+
try {
|
|
4093
|
+
const before = await sourceInventory(
|
|
4094
|
+
options.codexHome,
|
|
4095
|
+
options.includeArchived,
|
|
4096
|
+
);
|
|
4097
|
+
const snapshot = await collectUsageAttempt(options, before, onProgress);
|
|
4098
|
+
return snapshot;
|
|
4099
|
+
} catch (error) {
|
|
4100
|
+
if (!isSourceMutationError(error)) {
|
|
4101
|
+
throw error;
|
|
4102
|
+
}
|
|
4103
|
+
if (attempt === SOURCE_COLLECTION_MAX_ATTEMPTS) {
|
|
4104
|
+
const exhausted = new Error(
|
|
4105
|
+
`Local Codex sources changed during collection after ${SOURCE_COLLECTION_MAX_ATTEMPTS} attempts; no snapshot was published.`,
|
|
4106
|
+
{ cause: error },
|
|
4107
|
+
);
|
|
4108
|
+
exhausted.code = "ERR_SOURCE_CHANGED_DURING_COLLECTION";
|
|
4109
|
+
throw exhausted;
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
const error = new Error(
|
|
4115
|
+
`Local Codex sources changed during collection after ${SOURCE_COLLECTION_MAX_ATTEMPTS} attempts; no snapshot was published.`,
|
|
4116
|
+
);
|
|
4117
|
+
error.code = "ERR_SOURCE_CHANGED_DURING_COLLECTION";
|
|
4118
|
+
throw error;
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
export async function collectUsageSequential(options, onProgress = () => {}) {
|
|
4122
|
+
for (let attempt = 1; attempt <= SOURCE_COLLECTION_MAX_ATTEMPTS; attempt += 1) {
|
|
4123
|
+
const inventory = await sourceInventory(
|
|
4124
|
+
options.codexHome,
|
|
4125
|
+
options.includeArchived,
|
|
4126
|
+
);
|
|
4127
|
+
try {
|
|
4128
|
+
const snapshot = await collectUsageWithConcurrency(
|
|
4129
|
+
options,
|
|
4130
|
+
inventory,
|
|
4131
|
+
onProgress,
|
|
4132
|
+
1,
|
|
4133
|
+
false,
|
|
4134
|
+
);
|
|
4135
|
+
return snapshot;
|
|
4136
|
+
} catch (error) {
|
|
4137
|
+
if (!isSourceMutationError(error)) throw error;
|
|
4138
|
+
if (attempt === SOURCE_COLLECTION_MAX_ATTEMPTS) {
|
|
4139
|
+
const exhausted = new Error(
|
|
4140
|
+
`Local Codex sources changed during collection after ${SOURCE_COLLECTION_MAX_ATTEMPTS} attempts; no snapshot was published.`,
|
|
4141
|
+
{ cause: error },
|
|
4142
|
+
);
|
|
4143
|
+
exhausted.code = "ERR_SOURCE_CHANGED_DURING_COLLECTION";
|
|
4144
|
+
throw exhausted;
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
throw new Error("Unreachable sequential collection state.");
|
|
4149
|
+
}
|
|
4150
|
+
|
|
1358
4151
|
async function main() {
|
|
1359
4152
|
let options;
|
|
1360
4153
|
try {
|
|
1361
4154
|
options = parseArgs(process.argv.slice(2));
|
|
1362
4155
|
} catch (error) {
|
|
1363
|
-
process.stderr.write(`${error.message}\n\n${usage()}\n`);
|
|
4156
|
+
process.stderr.write(`${safeExportLabel(error.message, 2_000, "Invalid collector arguments")}\n\n${usage()}\n`);
|
|
1364
4157
|
process.exitCode = 1;
|
|
1365
4158
|
return;
|
|
1366
4159
|
}
|
|
@@ -1373,14 +4166,23 @@ async function main() {
|
|
|
1373
4166
|
}
|
|
1374
4167
|
|
|
1375
4168
|
process.stdout.write("Token Ledger: scanning local Codex metadata…\n");
|
|
1376
|
-
|
|
4169
|
+
let writeResult = null;
|
|
4170
|
+
const snapshot = await collectUsage({
|
|
4171
|
+
...options,
|
|
4172
|
+
stageSnapshot: async (candidate) => {
|
|
4173
|
+
process.stdout.write("\nToken Ledger: writing privacy-reduced snapshot…\n");
|
|
4174
|
+
writeResult = await stagePrivateSnapshot(options.output, candidate, {
|
|
4175
|
+
reservedPaths: durableLedgerRelatedPaths(options),
|
|
4176
|
+
});
|
|
4177
|
+
return writeResult;
|
|
4178
|
+
},
|
|
4179
|
+
}, ({ current, total }) => {
|
|
1377
4180
|
process.stdout.write(
|
|
1378
4181
|
`\rToken Ledger: scanned ${current.toLocaleString()}/${total.toLocaleString()} rollout files`,
|
|
1379
4182
|
);
|
|
1380
4183
|
});
|
|
1381
|
-
process.stdout.write("\
|
|
1382
|
-
const
|
|
1383
|
-
const storedSnapshot = writeResult.snapshot;
|
|
4184
|
+
process.stdout.write("\n");
|
|
4185
|
+
const storedSnapshot = snapshot;
|
|
1384
4186
|
process.stdout.write(
|
|
1385
4187
|
[
|
|
1386
4188
|
`Snapshot: ${options.output}`,
|
|
@@ -1388,6 +4190,8 @@ async function main() {
|
|
|
1388
4190
|
`Unique model calls: ${storedSnapshot.coverage.observedModelCalls.toLocaleString()}`,
|
|
1389
4191
|
`Stored usage buckets: ${storedSnapshot.events.length.toLocaleString()}`,
|
|
1390
4192
|
`Duplicate/copied events skipped: ${storedSnapshot.coverage.duplicateEventsSkipped.toLocaleString()}`,
|
|
4193
|
+
`Invalid token records excluded: ${storedSnapshot.coverage.invalidTokenRecords.toLocaleString()}`,
|
|
4194
|
+
`Invalid quota records excluded: ${storedSnapshot.coverage.invalidQuotaRecords.toLocaleString()}`,
|
|
1391
4195
|
`Threads with unresolved state-only counters: ${storedSnapshot.coverage.unresolvedThreadCounters.toLocaleString()}`,
|
|
1392
4196
|
`Snapshot size: ${(writeResult.bytesWritten / 1_000_000).toFixed(1)} MB (${writeResult.encoding}; ${(writeResult.jsonBytes / 1_000_000).toFixed(1)} MB JSON before encoding)`,
|
|
1393
4197
|
`Snapshot safety limit: ${(writeResult.maxBytes / 1_000_000).toFixed(1)} MB`,
|
|
@@ -1395,12 +4199,49 @@ async function main() {
|
|
|
1395
4199
|
);
|
|
1396
4200
|
}
|
|
1397
4201
|
|
|
4202
|
+
if (!isMainThread && workerData?.tokenLedgerImporterWorker && parentPort) {
|
|
4203
|
+
parentPort.on("message", async ({
|
|
4204
|
+
index,
|
|
4205
|
+
path,
|
|
4206
|
+
continuityBytes,
|
|
4207
|
+
maximumBytes,
|
|
4208
|
+
expectedIdentity,
|
|
4209
|
+
stateEntry,
|
|
4210
|
+
}) => {
|
|
4211
|
+
try {
|
|
4212
|
+
const result = await scanRollout(
|
|
4213
|
+
path,
|
|
4214
|
+
stateEntry ? new Map([stateEntry]) : new Map(),
|
|
4215
|
+
undefined,
|
|
4216
|
+
continuityBytes,
|
|
4217
|
+
maximumBytes,
|
|
4218
|
+
expectedIdentity,
|
|
4219
|
+
);
|
|
4220
|
+
parentPort.postMessage({ index, result });
|
|
4221
|
+
} catch (error) {
|
|
4222
|
+
parentPort.postMessage({
|
|
4223
|
+
index,
|
|
4224
|
+
error: {
|
|
4225
|
+
name: error?.name,
|
|
4226
|
+
message: error?.message || String(error),
|
|
4227
|
+
stack: error?.stack,
|
|
4228
|
+
code: error?.code,
|
|
4229
|
+
},
|
|
4230
|
+
});
|
|
4231
|
+
}
|
|
4232
|
+
});
|
|
4233
|
+
}
|
|
4234
|
+
|
|
1398
4235
|
const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : "";
|
|
1399
|
-
if (import.meta.url === invokedPath) {
|
|
4236
|
+
if (isMainThread && import.meta.url === invokedPath) {
|
|
1400
4237
|
main().catch((error) => {
|
|
1401
4238
|
process.stderr.write(
|
|
1402
4239
|
`Token Ledger collector failed: ${
|
|
1403
|
-
|
|
4240
|
+
safeExportLabel(
|
|
4241
|
+
error instanceof Error ? error.message : String(error),
|
|
4242
|
+
2_000,
|
|
4243
|
+
"Unknown collector error",
|
|
4244
|
+
)
|
|
1404
4245
|
}\n`,
|
|
1405
4246
|
);
|
|
1406
4247
|
process.exitCode = 1;
|