@vibe-cafe/vibe-usage 0.9.15 → 0.9.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/parsers/codex.js +83 -18
package/package.json
CHANGED
package/src/parsers/codex.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createReadStream, readdirSync, existsSync } from 'node:fs';
|
|
1
|
+
import { createReadStream, readdirSync, existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
@@ -42,9 +42,15 @@ function findJsonlFiles(dir) {
|
|
|
42
42
|
return results;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function readLines(filePath) {
|
|
45
|
+
function readLines(filePath, snapshotSize) {
|
|
46
46
|
return createInterface({
|
|
47
|
-
input: createReadStream(filePath, {
|
|
47
|
+
input: createReadStream(filePath, {
|
|
48
|
+
encoding: 'utf-8',
|
|
49
|
+
// Rollouts are append-only while Codex is working. Bound both parser
|
|
50
|
+
// passes to the size captured before pass 1 so they see the same prefix
|
|
51
|
+
// even when the live file grows between reads.
|
|
52
|
+
...(snapshotSize == null ? {} : { end: snapshotSize - 1 }),
|
|
53
|
+
}),
|
|
48
54
|
crlfDelay: Infinity,
|
|
49
55
|
});
|
|
50
56
|
}
|
|
@@ -143,6 +149,34 @@ function longestReplayPrefix(child, parent) {
|
|
|
143
149
|
return matched;
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Return the longest prefix of `child` found contiguously anywhere in
|
|
154
|
+
* `parent`. A live sub-agent rollout can be observed while Codex is still
|
|
155
|
+
* copying the parent block, before that copy reaches the parent snapshot's
|
|
156
|
+
* end. In that state the exact records are inherited history even though the
|
|
157
|
+
* stricter completed-replay suffix match above deliberately rejects them.
|
|
158
|
+
*/
|
|
159
|
+
function longestPartialReplayPrefix(child, parent) {
|
|
160
|
+
if (child.length === 0 || parent.length === 0) return 0;
|
|
161
|
+
|
|
162
|
+
const prefix = new Array(child.length).fill(0);
|
|
163
|
+
for (let i = 1, matched = 0; i < child.length; i++) {
|
|
164
|
+
while (matched > 0 && child[i] !== child[matched]) matched = prefix[matched - 1];
|
|
165
|
+
if (child[i] === child[matched]) matched++;
|
|
166
|
+
prefix[i] = matched;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let matched = 0;
|
|
170
|
+
let longest = 0;
|
|
171
|
+
for (const fingerprint of parent) {
|
|
172
|
+
while (matched > 0 && fingerprint !== child[matched]) matched = prefix[matched - 1];
|
|
173
|
+
if (fingerprint === child[matched]) matched++;
|
|
174
|
+
longest = Math.max(longest, matched);
|
|
175
|
+
if (matched === child.length) matched = prefix[matched - 1];
|
|
176
|
+
}
|
|
177
|
+
return longest;
|
|
178
|
+
}
|
|
179
|
+
|
|
146
180
|
// `task_started.started_at` is stored at one-second precision while the
|
|
147
181
|
// canonical session timestamp has milliseconds. Real Codex Desktop rollouts
|
|
148
182
|
// start the child task within a few seconds of creating the child session.
|
|
@@ -160,7 +194,7 @@ const OWN_TASK_START_WINDOW_MS = 5_000;
|
|
|
160
194
|
* full prefix. Together they bound matching to source records that existed at
|
|
161
195
|
* spawn without over-skipping child work when the parent later grows.
|
|
162
196
|
*/
|
|
163
|
-
async function indexSessionFile(filePath) {
|
|
197
|
+
async function indexSessionFile(filePath, snapshotSize) {
|
|
164
198
|
let sessionId = null;
|
|
165
199
|
let forkedFromId = null;
|
|
166
200
|
let parentThreadId = null;
|
|
@@ -178,7 +212,7 @@ async function indexSessionFile(filePath) {
|
|
|
178
212
|
let firstTaskBoundary = null;
|
|
179
213
|
let ownTaskBoundary = null;
|
|
180
214
|
|
|
181
|
-
for await (const line of readLines(filePath)) {
|
|
215
|
+
for await (const line of readLines(filePath, snapshotSize)) {
|
|
182
216
|
if (!line.trim()) continue;
|
|
183
217
|
try {
|
|
184
218
|
const obj = JSON.parse(line);
|
|
@@ -261,12 +295,13 @@ function replayBoundary(meta, sessionById) {
|
|
|
261
295
|
const parentAtSpawn = parent && meta.sessionStartedAtMs != null
|
|
262
296
|
? upperBound(parent.tokenTimes, meta.sessionStartedAtMs)
|
|
263
297
|
: null;
|
|
264
|
-
const
|
|
265
|
-
?
|
|
266
|
-
:
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
)
|
|
298
|
+
const parentSnapshot = parentAtSpawn == null
|
|
299
|
+
? []
|
|
300
|
+
: parent.tokenFingerprints.slice(0, parentAtSpawn);
|
|
301
|
+
const replayTokenCount = longestReplayPrefix(meta.tokenFingerprints, parentSnapshot);
|
|
302
|
+
const partialReplayTokenCount = meta.isSubagent
|
|
303
|
+
? longestPartialReplayPrefix(meta.tokenFingerprints, parentSnapshot)
|
|
304
|
+
: 0;
|
|
270
305
|
|
|
271
306
|
if (meta.isSubagent) {
|
|
272
307
|
// Direct evidence inside the child wins. Legacy single-meta rollouts did
|
|
@@ -292,11 +327,25 @@ function replayBoundary(meta, sessionById) {
|
|
|
292
327
|
: null);
|
|
293
328
|
if (direct) {
|
|
294
329
|
return {
|
|
295
|
-
rawTokenCount: Math.max(
|
|
330
|
+
rawTokenCount: Math.max(
|
|
331
|
+
replayTokenCount,
|
|
332
|
+
partialReplayTokenCount,
|
|
333
|
+
direct.rawTokenCount
|
|
334
|
+
),
|
|
296
335
|
recordIndex: direct.recordIndex,
|
|
297
336
|
};
|
|
298
337
|
}
|
|
299
|
-
|
|
338
|
+
|
|
339
|
+
// A recognized sub-agent can be synced while Codex is only partway
|
|
340
|
+
// through appending the copied parent block. The completed-replay matcher
|
|
341
|
+
// correctly rejects that interior slice, but counting it would create a
|
|
342
|
+
// temporary spike that disappears on the next sync. Exact payload overlap
|
|
343
|
+
// with the known parent is sufficient evidence to defer those leading
|
|
344
|
+
// records until the rollout reaches a stable suffix or task boundary.
|
|
345
|
+
return {
|
|
346
|
+
rawTokenCount: Math.max(replayTokenCount, partialReplayTokenCount),
|
|
347
|
+
recordIndex: null,
|
|
348
|
+
};
|
|
300
349
|
}
|
|
301
350
|
|
|
302
351
|
if (meta.forkedFromId) {
|
|
@@ -311,7 +360,16 @@ export async function parse() {
|
|
|
311
360
|
|
|
312
361
|
const entries = [];
|
|
313
362
|
const sessionEvents = [];
|
|
314
|
-
const files =
|
|
363
|
+
const files = [];
|
|
364
|
+
for (const filePath of dirs.flatMap(findJsonlFiles)) {
|
|
365
|
+
try {
|
|
366
|
+
const snapshotSize = statSync(filePath).size;
|
|
367
|
+
if (snapshotSize > 0) files.push({ filePath, snapshotSize });
|
|
368
|
+
} catch {
|
|
369
|
+
// The file may move to archived_sessions between discovery and stat.
|
|
370
|
+
// Its archived copy will be picked up on the next sync.
|
|
371
|
+
}
|
|
372
|
+
}
|
|
315
373
|
if (files.length === 0) return { buckets: [], sessions: [] };
|
|
316
374
|
|
|
317
375
|
// Pass 1: build a compact per-file index. Keep the most complete physical
|
|
@@ -319,10 +377,10 @@ export async function parse() {
|
|
|
319
377
|
// sessions/ and archived_sessions/ cannot double its token buckets.
|
|
320
378
|
const sessionById = new Map();
|
|
321
379
|
const fileMeta = new Map();
|
|
322
|
-
for (const filePath of files) {
|
|
380
|
+
for (const { filePath, snapshotSize } of files) {
|
|
323
381
|
let meta;
|
|
324
382
|
try {
|
|
325
|
-
meta = await indexSessionFile(filePath);
|
|
383
|
+
meta = await indexSessionFile(filePath, snapshotSize);
|
|
326
384
|
} catch {
|
|
327
385
|
continue;
|
|
328
386
|
}
|
|
@@ -336,7 +394,7 @@ export async function parse() {
|
|
|
336
394
|
}
|
|
337
395
|
|
|
338
396
|
// Pass 2: parse usage while skipping the replay prefix resolved above.
|
|
339
|
-
for (const filePath of files) {
|
|
397
|
+
for (const { filePath, snapshotSize } of files) {
|
|
340
398
|
const fm = fileMeta.get(filePath);
|
|
341
399
|
if (!fm) continue;
|
|
342
400
|
if (fm.sessionId && sessionById.get(fm.sessionId)?.filePath !== filePath) continue;
|
|
@@ -357,7 +415,7 @@ export async function parse() {
|
|
|
357
415
|
let turnContextModel = 'unknown';
|
|
358
416
|
let prevTotal = null;
|
|
359
417
|
let prevCumulativeTotal = null;
|
|
360
|
-
for await (const line of readLines(filePath)) {
|
|
418
|
+
for await (const line of readLines(filePath, snapshotSize)) {
|
|
361
419
|
if (!line.trim()) continue;
|
|
362
420
|
try {
|
|
363
421
|
const obj = JSON.parse(line);
|
|
@@ -483,6 +541,13 @@ export async function parse() {
|
|
|
483
541
|
continue;
|
|
484
542
|
}
|
|
485
543
|
}
|
|
544
|
+
|
|
545
|
+
// The byte bound makes append-only growth invisible to both passes. If a
|
|
546
|
+
// rollout was instead truncated or replaced in place, fail the Codex
|
|
547
|
+
// parser rather than upload totals produced from two different snapshots.
|
|
548
|
+
if (parsedRecordIndex !== fm.parsedRecordCount || rawTokenSeen !== fm.rawTokenCount) {
|
|
549
|
+
throw new Error('Codex rollout changed while syncing; retry on the next sync');
|
|
550
|
+
}
|
|
486
551
|
}
|
|
487
552
|
|
|
488
553
|
return { buckets: aggregateToBuckets(entries), sessions: extractSessions(sessionEvents) };
|