@token-dashboard/codex-usage-uploader 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@token-dashboard/codex-usage-uploader",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Codex 用量上报 CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "README.md"
13
13
  ],
14
14
  "scripts": {
15
+ "dev": "node bin/codex-usage-uploader.js init --backend-url http://localhost:8086",
15
16
  "test": "node --no-warnings --test tests/*.test.mjs"
16
17
  },
17
18
  "engines": {
package/src/cli.js CHANGED
@@ -257,44 +257,21 @@ function formatDuration(durationMs) {
257
257
  function printCatchUpProgress(event) {
258
258
  switch (event.phase) {
259
259
  case 'start':
260
- console.log(
261
- `[catch-up] start files=${event.totalFiles} queued_batches=${
262
- event.bufferingBatchCount +
263
- event.pendingBatchCount +
264
- event.retryingBatchCount
265
- } queued_events=${event.queuedEvents}`,
266
- );
260
+ console.log(`[scan] start files=${event.totalFiles}`);
267
261
  return;
268
262
  case 'file':
269
263
  console.log(
270
- `[catch-up] file ${event.filesProcessed}/${event.totalFiles} current=${event.file} events=${event.eventsParsed} queued_batches=${
271
- event.bufferingBatchCount +
272
- event.pendingBatchCount +
273
- event.retryingBatchCount
274
- } queued_events=${event.queuedEvents}`,
275
- );
276
- return;
277
- case 'upload':
278
- console.log(
279
- `[catch-up] uploaded_batches=${event.batchesUploaded} last_batch=${event.batchKey} remaining_batches=${
280
- event.bufferingBatchCount +
281
- event.pendingBatchCount +
282
- event.retryingBatchCount
283
- } remaining_events=${event.queuedEvents}`,
264
+ `[scan] file ${event.filesProcessed}/${event.totalFiles} current=${event.file} events=${event.eventsParsed}`,
284
265
  );
285
266
  return;
286
267
  case 'done':
287
268
  console.log(
288
- `[catch-up] complete files=${event.filesProcessed}/${event.totalFiles} events=${event.eventsParsed} uploaded_batches=${event.batchesUploaded} remaining_batches=${event.remainingQueuedBatches} remaining_events=${event.remainingQueuedEvents} duration=${formatDuration(event.durationMs)}`,
269
+ `[scan] complete files=${event.filesProcessed}/${event.totalFiles} events=${event.eventsParsed} pending_batches=${event.pendingBatches} duration=${formatDuration(event.durationMs)}`,
289
270
  );
290
271
  return;
291
272
  case 'error':
292
273
  console.error(
293
- `[catch-up] failed stage=${event.stage} message=${event.message} remaining_batches=${
294
- event.bufferingBatchCount +
295
- event.pendingBatchCount +
296
- event.retryingBatchCount
297
- } remaining_events=${event.queuedEvents}`,
274
+ `[scan] failed stage=${event.stage} message=${event.message}`,
298
275
  );
299
276
  return;
300
277
  }
@@ -351,10 +328,11 @@ async function runInit(options) {
351
328
  console.log(`Install root: ${runtime.installRoot}`);
352
329
  console.log(`Config file: ${runtime.configFile}`);
353
330
  printIdentity(identity);
354
- console.log('Starting foreground historical catch-up.');
331
+ console.log('Scanning local Codex sessions...');
355
332
 
333
+ let scanResult;
356
334
  try {
357
- await uploader.runForegroundCatchUp({
335
+ scanResult = await uploader.runForegroundCatchUp({
358
336
  onProgress: printCatchUpProgress,
359
337
  });
360
338
  } catch (error) {
@@ -363,6 +341,11 @@ async function runInit(options) {
363
341
 
364
342
  manager.start();
365
343
  console.log(`${PRODUCT_NAME} initialized and started.`);
344
+ if (scanResult.pendingBatches > 0) {
345
+ console.log(
346
+ `Background service is uploading ${scanResult.pendingBatches} batch(es) with ${scanResult.pendingEvents} event(s).`,
347
+ );
348
+ }
366
349
  console.log(`Use \`${CLI_NAME} status\` to check the local service.`);
367
350
  printPathHint(runtime);
368
351
  } finally {
package/src/collector.js CHANGED
@@ -465,7 +465,7 @@ export class CodexUsageUploader {
465
465
  const response = await fetch(`${this.backendUrl}${apiPath}`, {
466
466
  method: 'POST',
467
467
  headers: { 'Content-Type': 'application/json' },
468
- body: stableStringify(payload),
468
+ body: JSON.stringify(payload),
469
469
  });
470
470
  if (!response.ok) {
471
471
  const text = await response.text();
@@ -489,7 +489,7 @@ export class CodexUsageUploader {
489
489
  this.stateDb.setCheckpoint('last_register_at', String(nowTs()));
490
490
  }
491
491
 
492
- async flushPendingBatches({ failFast = false, onBatchUploaded } = {}) {
492
+ async flushPendingBatches({ failFast = false, concurrency = 5, onBatchUploaded } = {}) {
493
493
  if (!this.backendUrl) {
494
494
  return { uploadedBatches: 0, failedBatches: 0, lastError: null };
495
495
  }
@@ -509,42 +509,70 @@ export class CodexUsageUploader {
509
509
  let uploadedBatches = 0;
510
510
  let failedBatches = 0;
511
511
  let lastError = null;
512
+ const collectorBody = this.collectorRequestBody();
512
513
 
513
- for (const row of this.stateDb.iterDuePendingBatches()) {
514
+ const rows = this.stateDb.iterDuePendingBatches();
515
+
516
+ const uploadOne = async (row) => {
514
517
  const payload = this.sanitizeUploadPayload(JSON.parse(row.payload_json));
515
518
  const requestBody = {
516
519
  idempotencyKey: row.batch_key,
517
- collector: this.collectorRequestBody(),
520
+ collector: collectorBody,
518
521
  payloadSizeBytes: Number(row.payload_bytes),
519
522
  sessions: payload.sessions ?? [],
520
523
  turns: payload.turns ?? [],
521
524
  events: payload.events ?? [],
522
525
  };
526
+ await this.postJson('/codex-usage/upload', requestBody);
527
+ };
528
+
529
+ const pending = new Set();
530
+ let rowIndex = 0;
531
+
532
+ const enqueue = () => {
533
+ while (pending.size < concurrency && rowIndex < rows.length) {
534
+ const row = rows[rowIndex++];
535
+ const task = uploadOne(row)
536
+ .then(() => {
537
+ this.stateDb.markBatchUploaded(row.id);
538
+ uploadedBatches += 1;
539
+ onBatchUploaded?.({ row, uploadedBatches });
540
+ })
541
+ .catch((error) => {
542
+ this.stateDb.markBatchFailed(
543
+ row.id,
544
+ Number(row.attempt_count) + 1,
545
+ error instanceof Error ? error.message : String(error),
546
+ );
547
+ failedBatches += 1;
548
+ lastError = error;
549
+ if (failFast) {
550
+ throw error;
551
+ }
552
+ })
553
+ .finally(() => {
554
+ pending.delete(task);
555
+ });
556
+ pending.add(task);
557
+ }
558
+ };
559
+
560
+ enqueue();
561
+ while (pending.size > 0) {
523
562
  try {
524
- await this.postJson('/codex-usage/upload', requestBody);
525
- this.stateDb.markBatchUploaded(row.id);
526
- uploadedBatches += 1;
527
- onBatchUploaded?.({
528
- row,
529
- uploadedBatches,
530
- queueStats: this.getQueueStats(),
531
- });
532
- } catch (error) {
533
- this.stateDb.markBatchFailed(
534
- row.id,
535
- Number(row.attempt_count) + 1,
536
- error instanceof Error ? error.message : String(error),
537
- );
538
- failedBatches += 1;
539
- lastError = error;
540
- if (failFast) {
541
- throw new Error(
542
- `Upload batch ${row.batch_key} failed: ${
543
- error instanceof Error ? error.message : String(error)
544
- }`,
545
- );
546
- }
563
+ await Promise.race(pending);
564
+ } catch {
565
+ if (failFast) break;
547
566
  }
567
+ enqueue();
568
+ }
569
+
570
+ if (failFast && lastError) {
571
+ throw new Error(
572
+ `Upload batch failed: ${
573
+ lastError instanceof Error ? lastError.message : String(lastError)
574
+ }`,
575
+ );
548
576
  }
549
577
 
550
578
  return { uploadedBatches, failedBatches, lastError };
@@ -578,15 +606,12 @@ export class CodexUsageUploader {
578
606
  filesProcessed: 0,
579
607
  eventsParsed: 0,
580
608
  batchesQueued: 0,
581
- batchesUploaded: 0,
582
- ...this.getQueueStats(),
583
609
  });
584
610
 
585
611
  let scanResult;
586
612
  try {
587
613
  scanResult = await this.scanSnapshotEntries(backfillSnapshot, {
588
614
  onFileProcessed: ({ entry, filesProcessed, totals }) => {
589
- const queueStats = this.getQueueStats();
590
615
  onProgress?.({
591
616
  phase: 'file',
592
617
  file: entry.progressPath ?? entry.relpath,
@@ -594,8 +619,6 @@ export class CodexUsageUploader {
594
619
  filesProcessed,
595
620
  eventsParsed: totals.events,
596
621
  batchesQueued: totals.batchesQueued,
597
- batchesUploaded: 0,
598
- ...queueStats,
599
622
  });
600
623
  },
601
624
  });
@@ -604,44 +627,13 @@ export class CodexUsageUploader {
604
627
  phase: 'error',
605
628
  stage: 'scan',
606
629
  message: error instanceof Error ? error.message : String(error),
607
- ...this.getQueueStats(),
608
630
  });
609
631
  throw error;
610
632
  }
611
633
 
612
634
  scanResult.batchesQueued += this.stateDb.sealStaleBatches(true);
613
- this.stateDb.markAllPendingDue();
614
635
 
615
- let flushResult;
616
- try {
617
- flushResult = await this.flushPendingBatches({
618
- failFast: true,
619
- onBatchUploaded: ({ row, uploadedBatches, queueStats }) => {
620
- onProgress?.({
621
- phase: 'upload',
622
- batchKey: row.batch_key,
623
- totalFiles,
624
- filesProcessed: totalFiles,
625
- eventsParsed: scanResult.events,
626
- batchesQueued: scanResult.batchesQueued,
627
- batchesUploaded: uploadedBatches,
628
- ...queueStats,
629
- });
630
- },
631
- });
632
- } catch (error) {
633
- onProgress?.({
634
- phase: 'error',
635
- stage: 'upload',
636
- message: error instanceof Error ? error.message : String(error),
637
- totalFiles,
638
- filesProcessed: totalFiles,
639
- eventsParsed: scanResult.events,
640
- batchesQueued: scanResult.batchesQueued,
641
- ...this.getQueueStats(),
642
- });
643
- throw error;
644
- }
636
+ await this.ensureRemoteRegistration();
645
637
 
646
638
  const queueStats = this.getQueueStats();
647
639
  const result = {
@@ -651,14 +643,10 @@ export class CodexUsageUploader {
651
643
  sessionsParsed: scanResult.sessions,
652
644
  turnsParsed: scanResult.turns,
653
645
  batchesQueued: scanResult.batchesQueued,
654
- batchesUploaded: flushResult.uploadedBatches,
655
- remainingQueuedBatches:
656
- queueStats.bufferingBatchCount +
657
- queueStats.pendingBatchCount +
658
- queueStats.retryingBatchCount,
659
- remainingQueuedEvents: queueStats.queuedEvents,
646
+ pendingBatches:
647
+ queueStats.pendingBatchCount + queueStats.retryingBatchCount,
648
+ pendingEvents: queueStats.queuedEvents,
660
649
  durationMs: Date.now() - startedAt,
661
- ...queueStats,
662
650
  };
663
651
 
664
652
  onProgress?.({